Components · Advanced interaction
ReorderableList
A list reordered by dragging a handle (mouse, pen, touch) or fully from the keyboard, with every step announced.
Drag or keyboard
- New12
- Contacted8
- Proposal sent5
- Negotiation3
import { useState } from 'react';
import { Badge } from '@rookcom/monarch-react';
import { ReorderableList } from '@rookcom/monarch-react/data';
interface Stage {
id: string;
name: string;
count: number;
locked?: boolean;
}
const STAGES: Stage[] = [
{ id: 'new', name: 'New', count: 12 },
{ id: 'contacted', name: 'Contacted', count: 8 },
{ id: 'proposal', name: 'Proposal sent', count: 5 },
{ id: 'negotiation', name: 'Negotiation', count: 3 },
];
function StagesDemo() {
const [stages, setStages] = useState(STAGES);
return (
<div style={{ width: '100%', maxWidth: 420 }}>
<ReorderableList
aria-label="Pipeline stages"
items={stages}
onReorder={setStages}
getKey={(s) => s.id}
getLabel={(s) => s.name}
renderItem={(s) => (
<span style={{ display: 'flex', justifyContent: 'space-between', gap: 8 }}>
<span>{s.name}</span>
<Badge>{s.count}</Badge>
</span>
)}
/>
</div>
);
}
<StagesDemo />Usage examples
Move buttons and a locked item
import { useState } from 'react';
import { ReorderableList } from '@rookcom/monarch-react/data';
function ButtonsDemo() {
const [fields, setFields] = useState([
{ id: 'name', name: 'Full name', locked: true },
{ id: 'email', name: 'Email address' },
{ id: 'phone', name: 'Phone number' },
{ id: 'notes', name: 'Anything else?' },
]);
return (
<div style={{ width: '100%', maxWidth: 420 }}>
<ReorderableList
aria-label="Form fields"
items={fields}
onReorder={setFields}
moveButtons
isItemDisabled={(f) => f.locked === true}
getKey={(f) => f.id}
getLabel={(f) => f.name}
renderItem={(f, { disabled }) => (
<span>
{f.name}
{disabled ? ' (locked)' : null}
</span>
)}
/>
</div>
);
}
<ButtonsDemo />Purpose
Lets people put a short list in their own order, such as pipeline stages, form fields, banners or approval steps, by drag or keyboard, with the same result. Import from @rookcom/monarch-react/data.
Use when
- The order of a list is user data the app saves: stages, sections, fields, priorities.
- A settings list where “Move up / Move down” buttons are the accessible fallback today.
Avoid when
- Moving cards between columns (a board): this is a single vertical list.
- Sorting by a value: offer a sort control instead of manual order.
- Very long or virtualised lists: auto-scrolling while dragging is not provided.
Design principles
- Drag only from the grip handle, so text in the row stays selectable and buttons in it stay clickable.
- Keyboard is first-class, not a fallback: the same move happens with Space, arrows and Space, and Escape always undoes it.
- Say what happened: “Picked up”, “moved to position 3 of 5”, “dropped”, “cancelled” go to a live region.
- Nothing is saved until the drop:
onReorderfires once per finished move with the new order, ready for one API call. - Rows glide to their new place (a FLIP transition) unless the user prefers reduced motion.
- No drag-and-drop library: pointer events with capture cover mouse, pen and touch in a few hundred lines.
API reference
import { ReorderableList, moveItem } from '@rookcom/monarch-react/data';| Export | Kind | Rendering | Root class |
|---|---|---|---|
ReorderableList | component | client | .mn-reorderable- |
moveItem | utility | client | – |
Generic over the item type: ReorderableList<T> with getKey, getLabel and renderItem(item, { index, active, disabled }).
Renders <div class="mn-reorderable-list"> with a <ul> (the ref and the rest of the props go to the list) and two visually hidden helpers: the instructions and the live region.
onReorder(items, { item, from, to }) gets a new array; moveItem(list, from, to) is exported for apps that move items elsewhere (for example after an undo).
Generated from the TypeScript declarations of packages/react/src/components/interaction/reorderable-list.
Properties
ReorderableList
A list the user can reorder by dragging a handle (mouse, pen or touch) or from the keyboard: Space picks an item up, the arrow keys move it, Space drops it and Escape cancels. Every step is announced. Optional move buttons give a one-click alternative.
| Prop | Type | Description |
|---|---|---|
getKey required | (item: T) => string | A stable, unique key for each item. |
getLabel required | (item: T) => string | Plain-text name of each item, used in labels and announcements. |
renderItem required | (item: T, state: ReorderableListItemState) => ReactNode | Draws the row's content; the handle and move buttons are added around it. |
className | string | |
defaultItems | readonly T[] | The starting order when uncontrolled. |
disabled | boolean | No item can be moved: handles and buttons are disabled. |
id | string | |
isItemDisabled | ((item: T) => boolean) | An item that cannot be moved. Other items can still move past it. |
items | readonly T[] | The items in order (controlled). Update it from `onReorder`. |
messages | Partial<ReorderableListMessages> | Override labels and announcements. |
moveButtons | boolean | Also show Move up / Move down buttons on each row: a single-click alternative for people who cannot drag or hold keys. |
onReorder | ((items: T[], move: ReorderableListMove<T>) => void) | Called once per finished move (drop, Space, or a move button) with the new order. |
ref | ForwardedRef<HTMLUListElement> |
Variants
| Prop | Values | Description |
|---|---|---|
moveButtons | 'false''true' | Adds Move up / Move down buttons to each row (disabled at the ends): a single-click alternative for switch and voice users. |
isItemDisabled / disabled | '(item) => boolean''boolean' | Locks single items (others can still move past them) or the whole list. |
States
| State | Exposed as | Behaviour |
|---|---|---|
| Picked up (keyboard) | data-active="keyboard" on the row, aria-pressed="true" on the handle | Raised shadow and accent border; the handle is tinted. |
| Dragging (pointer) | data-active="pointer" on the row, data-dragging="pointer" on the root | The row follows the pointer with a raised shadow; others make room. |
| Locked | data-disabled on the row, disabled handle and buttons | The handle shows the disabled colour and a not-allowed cursor. |
| Focus | – | Handles and buttons show the Monarch focus ring on :focus-visible. |
Interaction behaviour
- Pointer: press the handle and move more than 4px to start; the row follows the pointer and the list reorders as its centre passes others; release to drop. pointercancel (for example a system gesture) cancels.
- The handle sets
touch-action: none, so on touch screens dragging the handle never scrolls the page; the rest of the row scrolls normally. - Keyboard moves are previewed in place and committed on drop; Escape, Tab away, or focus leaving the list cancels and restores the order.
- Controlled with
items+onReorder, or uncontrolled withdefaultItems. A controlled list that does not updateitemssnaps back. - If the moving item disappears from
itemsmid-move, the move ends quietly.
Keyboard support
| Keys | Action |
|---|---|
| Tab | Moves between handles (and move buttons). |
| Space / Enter | Picks up the focused item; pressed again, drops it. |
| ArrowUp / ArrowDown (or Left / Right) | Moves the picked-up item one place. |
| Home / End | Moves the picked-up item to the start or end. |
| Escape | Cancels the move (keyboard or pointer) and restores the order. |
Accessibility
- Role
- list with button handles
- Accessible name
- Give the list an
aria-label(“Pipeline stages”). Each handle is “Reorder <label>” (messages.handleLabel).
- Every handle is described by one visually hidden instruction (
aria-describedby). - Pick-up, each move, drop and cancel are announced in an assertive live region with the position (“position 3 of 5”).
- Focus stays on the moved item’s handle after every keyboard move and button press (or its handle when the button became disabled).
- Move buttons are named per item (“Move Email address up”).
- Verified with axe in jsdom and in Chromium, Firefox and WebKit on /components/reorderable-list.
Responsive behaviour
- Rows fill their container and wrap long content; nothing has a fixed width, so it works at 320px.
- On coarse pointers handles and move buttons grow to
--mn-control-md(40px) touch targets.
Theme support
- Monarch tokens only: surface rows with a subtle border; the active row uses
--mn-color-accent-borderand--mn-shadow-md. - Row transitions are removed under
prefers-reduced-motion: reduceand by the FLIP code itself. - Forced-colours mode outlines the active row in Highlight.
Do and don't
Do
- Give every item a short, unique
getLabel: it is what screen-reader users hear. - Save once in
onReorder, and roll back (pass the olditems) if the save fails. - Turn on
moveButtonsin settings screens where reordering is the main task.
Don't
- Do not put drag handlers on the whole row; keep the handle.
- Do not reorder on every intermediate move: wait for
onReorder. - Do not use it for boards with columns or for lists of hundreds of rows.
Related components
Dependencies
| Kind | Depends on |
|---|---|
| npm packages | react |
| Monarch components | None |
Testing coverage
- Named list, labelled and described handles, axe.
- Keyboard: Space picks up, arrows move with focus kept, Space drops and calls onReorder once with
{ item, from, to }; each step announced. - Escape cancels and restores; Home/End; moves stop at the edges; arrows do nothing before pick-up; Tab away cancels.
- Controlled: follows
items, and snaps back when the parent ignores onReorder. - Move buttons: disabled at the ends, announce, keep focus on the moved item; axe.
- Locked items and a disabled list; other items move past a locked one.
- Pointer: threshold before a drag, reorder by row centres, data-active, drop commits; pointercancel and Escape cancel.
- Translated messages;
moveItemclamps and copies; renders on the server.
| Check | Covered |
|---|---|
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery/reorderable-list; Cross-browser axe: /components/reorderable-list |
Tests: packages/react/src/components/interaction/reorderable-list