Skip to the content

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.

import { OrderList } from "@booleanpress/ui/order-list"

export default function OrderListEmpty() {
  return (
    <OrderList
      header="Escalation steps"
      defaultValue={[]}
      empty="No escalation steps yet"
      listClassName="h-40"
      className="w-full max-w-xs"
    />
  )
}

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" with aria-multiselectable="true"; each item is a role="option" with aria-selected. The highlighted item is the list's aria-activedescendant. The Move buttons are buttons named from the provider; a move that cannot happen is aria-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-label or aria-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 renderItem written inline is new on each render of its parent and redraws every item then: in a long list, define it outside the component or with useCallback.
  • 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

Keyboard
KeyBehaviour
โ†“orโ†‘Move the highlight to the next or the previous item, stopping at the ends.
HomeorEndMove the highlight to the first or the last item.
SpaceChooses 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).
ShiftSpaceChooses every item from the last one chosen to the highlighted one.
CtrlAChooses 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.
AltHomeMoves the chosen items to the top; Alt and End to the bottom.
EnterWith draggable, picks the highlighted item up; the arrow keys then move it, Space or Enter drops it and Escape puts it back.
Aโ€“ZType-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

OrderList props
PropTypeDefaultDescription
controls"none" | "start" | "end"startWhere the Move buttons sit: before the list (default), after it, or nowhere.
defaultSelectedstring[]The items chosen at first, when the list controls them.
defaultValuereadonly T[]The items in the order they start in, when the list controls itself.
disabledbooleanfalseGreys the list and its buttons, keeps the order, and takes the list out of the tab order.
draggablebooleanfalseLets people drag items to a new place, with a mouse, a finger (press and hold) or the keyboard (Enter).
dragHandlebooleanfalseWith draggable, shows a grip at the start of each item; only the grip starts a pointer drag.
emptyReactNodeWhat the list says when it has no items. Defaults to the provider's noItems string.
filterbooleanfalseShows a field above the list that keeps the items whose label contains its text.
filterPlaceholderstringThe filter field's placeholder.
filterValuestringThe 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.
headerReactNodeA title above the list that names it.
indicator"none" | "checkbox"nonecheckbox draws a checkbox on every item, ticked once chosen; a click then toggles an item.
listClassNamestringClasses 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.
selectAllbooleanfalseShows a "Select all" checkbox above the list that chooses every item in view, or none.
selectedstring[]The chosen items' keys, when you control them.
valuereadonly 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.

Theme tokens
TokenUsed for
--accentbackground
--accent-foregroundtext
--controlborder, background
--fieldbackground
--field-disabledbackground
--field-disabled-foregroundtext
--foregroundtext
--highlightbackground
--highlight-focusbackground
--highlight-foregroundtext, border
--invalidborder
--muted-foregroundtext
--popoverbackground
--popover-foregroundtext
--primarybackground, border
--primary-foregroundtext
--ringborder
--secondarybackground
--secondary-foregroundtext