Skip to content

ReorderableList

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/reorderable-list.json
See it liveA column picker whose rows drag by their grip, with the drop line - or focus a row and press Alt with an arrow.→

The columns in a column picker, the stops of a dial, the roles of a profile. A hook and a grip rather than a list component: the rows are already something else’s — a menu’s items, a form’s fields — and a component wrapping them would have to reproduce whatever that something else does.

const reorder = useReorder<ColumnId>({ order: columns, onMove })
<div {...reorder.listProps} className="relative">
{columns.map((id) => (
<MenuCheckboxItem
key={id}
checked
closeOnClick={false}
className={cn(reorder.dragging === id && 'opacity-50')}
{...reorder.rowProps(id)}
>
<span className="flex-1">{LABEL[id]}</span>
<ReorderGrip {...reorder.gripProps(id)} title={t('columns.moveHint')} />
</MenuCheckboxItem>
))}
<ReorderIndicator offset={reorder.slotOffset} />
</div>

Nothing moves until the pointer is let go. Live reordering looks better for a second and costs a write per crossed row — to a profile, that is a request per row — and the row being dragged has already been picked up, so the reader is watching the line that says where it lands. onMove(id, to) is called once per drop, with to the index in the list as it will be.

The line is yours to draw. The hook reports slot and slotOffset, the insertion point’s distance from the top of the element listProps is on; ReorderIndicator draws it there, which is why that element is relative. No line is drawn for a drop that would change nothing.

Alt with an arrow moves the focused row. Plain arrows are how a list is walked, and taking them for moving would leave no way to walk it; the modifier is the one screen readers and editors already use for “move this line”. The listener is native, on the row itself, so a menu popup that stops arrow keys on the way up cannot swallow it.

The grip is decorative. The keyboard path is on the row, so the grip carries no role and no label. The click a drop leaves behind is stopped: in a menu it would toggle the item that was only meant to be moved.

Pointer events, not HTML5 drag-and-drop, for the reason given at ColumnResizeHandle; the grip takes pointer capture so the rows underneath never see the drag.

useReorder({ order, onMove, disabled? }) returns listProps for the element holding the rows, rowProps(id) for each row (the id and the keyboard path), gripProps(id) for the grip, and dragging, slot and slotOffset for drawing the drag: the row being dragged, the insertion point in the order as it is, and its distance from the top of the list in pixels. ReorderGrip takes any <span> attribute, title for the hover hint.

Props

Read from the types in ui/reorderable-list.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.

ReorderGrip

The handle a row is picked up by. Decorative to a screen reader - the keyboard path is on the row itself - so it carries no role and no label.

Every prop of React's HTMLAttributes<HTMLElement>, passed on.

ReorderIndicator

The line where a dragged row would land. Positioned by the caller from slotOffset, inside the element listProps is on - which has to be positioned itself.

PropTypeDefault
offsetrequirednumber | nullThe line's distance from the top of the list, in pixels - slotOffset from useReorder. Nothing is drawn at null.
classNamestring

Accessibility

The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 6 rules applied and passed, and none failed. The accessibility report has every item.

History

Each release that changed ui/reorderable-list.tsx - the file a product copies - newest first.

  1. v0.41.0
    • docs read every props table from the component's types ee3b159
  2. v0.32.0
    • fix let an icon keep the size it is given, across the set 217b558
  3. v0.26.0
    • feat a header edge to drag, rows that reorder, a funnel with a panel 8310586