ComponentsData
Order list
A list people put in order: they choose items, then move them with buttons, Alt and the arrow keys, or by dragging.
Import
import { OrderList, OrderListGroup } from "@booleanpress/ui/order-list"Also install @dnd-kit/core, @dnd-kit/sortable: pnpm add @dnd-kit/core @dnd-kit/sortable
Usage
OrderList is a listbox of items with four Move buttons beside it. People choose one or several items, then move them up, down, to the top or to the bottom; with draggable they can also drag an item, by mouse, by touch (press and hold) or by keyboard. Install the peers first: npm install @dnd-kit/core @dnd-kit/sortable.
import { useState } from "react"
import { OrderList } from "@booleanpress/ui/order-list"
export function RoutingRules({ initial }: { initial: { id: string; name: string }[] }) {
const [rules, setRules] = useState(initial)
return <OrderList header="Routing rules" value={rules} onValueChange={setRules} draggable />
}The value is the items themselves, in order: controlled with value and onValueChange, or uncontrolled with defaultValue. Each item needs a stable key: strings are their own key, objects use id (then value), or pass getItemKey. getItemLabel gives the text that type-ahead, the filter and the announcements use (by default the string itself, else label, name or title); renderItem draws anything richer and is told the item's index, whether it is selected, and whether it is the dragging copy.
The chosen items are keys, controlled with selected and onSelectedChange or not. A click chooses one item; Ctrl or โ adds or removes one, Shift chooses a range. indicator="checkbox" draws a checkbox on every item, and a click then toggles; selectAll adds a "Select all" checkbox above the list. filter adds a field that narrows the items in view; moves then act on the items in view and leave the hidden ones in place.
Name the list with header (a title above it), aria-label or aria-labelledby. The frame is as tall as its items; listClassName="h-60" fixes the height and makes the list scroll. controls="end" puts the buttons after the list, controls="none" hides them. moveItems(items, keys, move) is the same move as a function, for buttons of your own. To drag items between lists, put the lists inside one OrderListGroup; PickList does this for two.
Examples
Basic
Choose a rule, then move it with the buttons or with Alt and the arrow keys; renderItem shows its priority.
import { useState } from "react"
import { OrderList } from "@booleanpress/ui/order-list"
const RULES = [
{ id: "password-resets", name: "Password resets โ Postmark" },
{ id: "receipts", name: "Receipts โ Amazon SES" },
{ id: "invoices", name: "Invoices โ Amazon SES" },
{ id: "sign-in-codes", name: "Sign-in codes โ Postmark" },
{ id: "newsletters", name: "Newsletters โ Mailgun" },
{ id: "digests", name: "Weekly digests โ Mailgun" },
{ id: "staff-alerts", name: "Staff alerts โ SMTP relay" },
{ id: "fallback", name: "Everything else โ SMTP relay" },
]
export default function OrderListBasic() {
const [rules, setRules] = useState(RULES)
return (
<OrderList
header="Routing rules, first match wins"
value={rules}
onValueChange={setRules}
renderItem={(rule, { index }) => (
<span className="flex min-w-0 items-center gap-3 text-sm">
<span className="w-4 shrink-0 text-end text-muted-foreground tabular-nums in-data-selected:text-current">{index + 1}</span>
<span className="truncate">{rule.name}</span>
</span>
)}
className="w-full max-w-sm"
/>
)
}Drag and drop
draggable: drag an item to a new place, or press Enter on it, move it with the arrow keys and drop it with Space.
import { OrderList } from "@booleanpress/ui/order-list"
const MAILERS = ["Amazon SES", "Postmark", "Mailgun", "SendGrid", "Brevo", "SMTP relay"]
export default function OrderListDragAndDrop() {
return (
<OrderList
header="Mailer fallback order"
defaultValue={MAILERS}
draggable
className="w-full max-w-xs"
/>
)
}Drag handle
dragHandle shows a grip on each item; only the grip starts a pointer drag.
import { OrderList } from "@booleanpress/ui/order-list"
const STEPS = [
{ id: "verify", label: "Verify the sender domain" },
{ id: "warm-up", label: "Warm up the dedicated IP" },
{ id: "import", label: "Import the contact list" },
{ id: "segment", label: "Build the first segment" },
{ id: "send", label: "Send the welcome series" },
]
export default function OrderListDragHandle() {
return (
<OrderList
header="Onboarding checklist"
defaultValue={STEPS}
draggable
dragHandle
className="w-full max-w-sm"
/>
)
}Multiple selection with checkboxes
indicator="checkbox": a click toggles an item, and the Move buttons move every chosen item together.
import { OrderList } from "@booleanpress/ui/order-list"
const COLUMNS = [
{ id: "recipient", label: "Recipient" },
{ id: "subject", label: "Subject" },
{ id: "status", label: "Status" },
{ id: "mailer", label: "Mailer" },
{ id: "opened", label: "Opened" },
{ id: "clicked", label: "Clicked" },
{ id: "sent-at", label: "Sent at" },
]
export default function OrderListCheckbox() {
return (
<OrderList
header="Delivery log columns"
defaultValue={COLUMNS}
defaultSelected={["status", "mailer"]}
indicator="checkbox"
draggable
className="w-full max-w-xs"
/>
)
}Filter
filter narrows the items in view; moves act on them and leave the hidden ones in place.
import { OrderList } from "@booleanpress/ui/order-list"
const SITES = [
"shop.example.com",
"blog.example.com",
"docs.example.com",
"status.example.com",
"help.example.com",
"careers.example.com",
"events.example.com",
"partners.example.com",
]
export default function OrderListFilter() {
return (
<OrderList
header="Sites, in sending order"
defaultValue={SITES}
filter
filterPlaceholder="Search sites"
listClassName="max-h-56"
className="w-full max-w-xs"
/>
)
}Empty placeholder
With no items the list says so, in empty or the provider's noItems text.
Disabled
disabled greys the list and its buttons, keeps the order and takes the list out of the tab order.
import { OrderList } from "@booleanpress/ui/order-list"
const QUEUES = ["Billing", "Technical support", "Sales", "Partnerships"]
export default function OrderListDisabled() {
return (
<OrderList
header="Ticket queues (locked by your plan)"
defaultValue={QUEUES}
defaultSelected={["Sales"]}
disabled
className="w-full max-w-xs"
/>
)
}Invalid
aria-invalid with the reason in a message that aria-describedby points to.
import { useState } from "react"
import { OrderList } from "@booleanpress/ui/order-list"
const QUEUES = ["Billing", "Technical support", "Sales", "Partnerships"]
export default function OrderListInvalid() {
const [moved, setMoved] = useState(false)
return (
<div className="flex w-full max-w-xs flex-col gap-2">
<OrderList
header="Ticket queues, first to last"
defaultValue={QUEUES}
onValueChange={() => setMoved(true)}
aria-invalid={!moved}
aria-describedby="order-list-invalid-error"
className="w-full"
/>
<p id="order-list-invalid-error" className="min-h-4 text-xs text-destructive-strong">
{moved ? "" : "Put Billing last before you save the routing."}
</p>
</div>
)
}Accessibility
- Semantics
- The list has
role="listbox"witharia-multiselectable="true"; each item is arole="option"witharia-selected. The highlighted item is the list'saria-activedescendant. The Move buttons are buttons named from the provider; a move that cannot happen isaria-disabled, so the focus stays on its button. Every move, by button, by key or by drag, is read out in a polite live region: "Password resets moved to position 2 of 8". - Labels
- Name the list with
header,aria-labeloraria-labelledby. The buttons are named Move to top, Move up, Move down and Move to bottom; the filter field Filter options and the header checkbox Select all; all from the provider. - Focus
- The buttons, the filter field and the list are tab stops of their own. Focus on the list highlights the first chosen item, else the first; the highlight shows only while the list has focus. A click chooses and leaves focus on the list. A drag never moves the focus.
- Known limits
- A pointer drag moves one item; the buttons and Alt with the arrows move every chosen item.
- Items are always in the page. A key press redraws only the items it changes, so a list of 5,000 keeps up with the keyboard; choosing every item, and each move in a draggable list, redraws them all. Beyond a few thousand, page or filter them on the server. A
renderItemwritten inline is new on each render of its parent and redraws every item then: in a long list, define it outside the component or withuseCallback. - Alt with the arrow keys can be taken by a screen reader or the browser in some set-ups; the buttons always work (WCAG 2.5.7).
Keyboard
| Key | Behaviour |
|---|---|
| โorโ | Move the highlight to the next or the previous item, stopping at the ends. |
| HomeorEnd | Move the highlight to the first or the last item. |
| Space | Chooses the highlighted item, or un-chooses it. |
| Shiftโ | Moves the highlight and chooses every item from the last one chosen to it (Shift and ArrowUp, Home or End too). |
| ShiftSpace | Chooses every item from the last one chosen to the highlighted one. |
| CtrlA | Chooses every item in view, or none when all are chosen (โ and A on a Mac). |
| Altโ | Moves the chosen items (or the highlighted one) one place up; Alt and ArrowDown one place down. |
| AltHome | Moves the chosen items to the top; Alt and End to the bottom. |
| Enter | With draggable, picks the highlighted item up; the arrow keys then move it, Space or Enter drops it and Escape puts it back. |
| AโZ | Type-ahead: moves the highlight to the next item whose label starts with the letters typed. |
| โ | In the filter field, moves the focus to the list. |
API
OrderList
| Prop | Type | Default | Description |
|---|---|---|---|
controls | "none" | "start" | "end" | start | Where the Move buttons sit: before the list (default), after it, or nowhere. |
defaultSelected | string[] | The items chosen at first, when the list controls them. | |
defaultValue | readonly T[] | The items in the order they start in, when the list controls itself. | |
disabled | boolean | false | Greys the list and its buttons, keeps the order, and takes the list out of the tab order. |
draggable | boolean | false | Lets people drag items to a new place, with a mouse, a finger (press and hold) or the keyboard (Enter). |
dragHandle | boolean | false | With draggable, shows a grip at the start of each item; only the grip starts a pointer drag. |
empty | ReactNode | What the list says when it has no items. Defaults to the provider's noItems string. | |
filter | boolean | false | Shows a field above the list that keeps the items whose label contains its text. |
filterPlaceholder | string | The filter field's placeholder. | |
filterValue | string | The filter's text, when you control it. | |
getItemKey | ((item: T) => string) | A stable, unique key for an item. Defaults to the item itself for strings, else its id, then its value. | |
getItemLabel | ((item: T) => string) | The item's text: what type-ahead and the filter match and what the announcements say. Defaults to the item itself
for strings, else its label, name or title. | |
header | ReactNode | A title above the list that names it. | |
indicator | "none" | "checkbox" | none | checkbox draws a checkbox on every item, ticked once chosen; a click then toggles an item. |
listClassName | string | Classes for the scrolling list inside the frame, such as a height (h-60) or a maximum height. | |
onFilterValueChange | ((value: string) => void) | Called with the filter's text as it is typed. | |
onSelectedChange | ((selected: string[]) => void) | Called with the chosen items' keys, in list order. | |
onValueChange | ((value: T[]) => void) | Called with the items in their new order after every move. | |
renderItem | ((item: T, state: OrderListItemState) => ReactNode) | Draws an item's content. Defaults to its label. | |
selectAll | boolean | false | Shows a "Select all" checkbox above the list that chooses every item in view, or none. |
selected | string[] | The chosen items' keys, when you control them. | |
value | readonly T[] | The items in their order, when you control it. Pair it with onValueChange. |
OrderListGroup
Every part takes className, merged with its defaults by cn(), and ref, which reaches the element it renders.
Data attributes: data-slot="order-list" (OrderList), data-slot="order-list-status" (OrderListGroup), and data-controls, data-disabled, data-drop-target, data-selected, data-highlighted, data-dragging, data-draggable, data-drop.
Provider strings: itemMoved, dragStarted, dragCancelled, itemsMoved, noItems, noResults, moveToTop, moveUp, moveDown, moveToBottom, selectAll, filterOptions, dragHandle (BooleanUIProvider's strings).
Theming
The frame takes the field tokens: --field, --control for the edge, --ring while the list has focus, --invalid when invalid and --field-disabled when disabled. Items use --accent under the pointer and while highlighted, and --highlight once chosen (--highlight-focus both). The buttons are the small secondary icon buttons. A dragged item lifts onto --popover with the overlay shadow; its place stays at half strength.
The tokens its classes read. Change their values in your theme, and it follows.
| Token | Used for |
|---|---|
--accent | background |
--accent-foreground | text |
--control | border, background |
--field | background |
--field-disabled | background |
--field-disabled-foreground | text |
--foreground | text |
--highlight | background |
--highlight-focus | background |
--highlight-foreground | text, border |
--invalid | border |
--muted-foreground | text |
--popover | background |
--popover-foreground | text |
--primary | background, border |
--primary-foreground | text |
--ring | border |
--secondary | background |
--secondary-foreground | text |