Skip to content
txbrendan/ui

Reorder List

beta

A list you put in order, like an on-call order or a meeting's running order. Drag a row by its handle, or pick it up with Space and move it with the arrows.

Preview

  1. Intros15 min · Everyone
  2. Roadmap update20 min · Product
  3. Design review20 min · Design
  4. Sales numbers15 min · Sales
  5. Demo of the week20 min · Everyone
  6. Wrap-up10 min

Each move can be undone with ⌘Z for 8 seconds, then it is saved.

On call
  1. Sam CarterEngineering · 14 shifts
  2. Noah KerrEngineering · 6 shifts
  3. Cal NashSupport
  4. Dan KerrNew hire
Reviewers · Order sent
  1. Ben WardDesign
  2. Owen PriceEngineering

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. Add the component.

    npx shadcn@latest add @txbrendan/reorder-list

Use it for

  • An on-call order, a meeting's running order, agenda items: one short list, in a Card.
  • Pass items with an id each. A person shows with their photo. Anything else shows its label and meta, or your renderItem.
  • Keep the order in state with onReorder. It hands back your own objects, in their new order, after each drop.
  • Tell people about the new order in onSettle. It runs once the 8-second ⌘Z window ends, so ⌘Z never takes back a message already sent.
  • Turn on numbered when the rank matters. Ranks and dividers belong to the slots and stay put while rows move.
  • Set disabled once the order is final, like once the order is sent. The handles go, so it no longer looks as if it could move.

Avoid it for

  • Placing people onto a team sheet's positions. That is a slot for each position, like PositionSlot.
  • Long lists that scroll. A drag does not scroll the page for you.
  • Your own undo button or timer. Each drop joins the page's ⌘Z stack, and moves made within 8 seconds undo together.

Props

  • items

    The rows, in order.

    T[] where T extends { id: string; person?: string; photo?: string; label?: string; meta?: string }

  • onReorder

    The items in their new order after each drop, and the order before when ⌘Z puts it back.

    (items: T[]) => void

  • onSettle

    The order is final: its ⌘Z window ended. Tell people here.

    (items: T[]) => void

  • renderItem

    Row content. Default a Person for people, otherwise the label and meta.

    (item: T, index: number) => ReactNode

  • numbered

    Shows each rank at the row's left.

    boolean · default false

  • label

    Names the list, like "On call, in order".

    string

  • disabled

    Locked. The rows show no handles.

    boolean · default false

  • className

    Extra classes. Later classes win.

    string

Accessibility

  • The list is an ordered list named by label, so screen readers say each row's place.
  • Each row's handle is a button named "Move" and the row's name. The handles share one Tab stop, and ↑↓, Home, and End move between them.
  • Space picks the focused row up, and its handle reads as pressed. ↑↓ move it, Home and End take it to the ends, Space or Enter drops it, and Esc puts it back.
  • A row that is up owns Esc, even inside a Sheet. The next Esc closes the sheet.
  • Focus leaving a row that is up drops it where it is.
  • A polite live region says each step: picked up, each place it passes, and where it dropped or went back.
  • Every drag has the same moves on the keys. A mouse lifts a row after 4 px, a finger after holding still for 350 ms, and Esc cancels a drag.
  • Reduced motion turns off the glide. The rows step to their new places.

Examples

A running order

const [plan, setPlan] = useState(PLAN);

<Card className="gap-0 py-1.5">
  <ReorderList items={plan} label="Tuesday's agenda, in order" numbered onReorder={setPlan} onSettle={save} />
</Card>

People in ranked order

<ReorderList
  items={[
    { id: "sam", person: "Sam Carter", meta: "Engineering" },
    { id: "noah", person: "Noah Kerr", meta: "Engineering" },
  ]}
  label="On call, in order"
  numbered
  onReorder={setOnCall}
/>