Components Advanced
SortableList
StableA list people can reorder by dragging, with the keyboard, or with move buttons, announcing every change.
Use SortableList when the order of items matters and people set it themselves: approval steps, signature fields, pinned templates. Dragging is one of three ways to reorder; the keyboard and the optional Move up and Move down buttons do the same job without a drag.
Overview
- Legal review
- Finance approval
- Director sign-off
- Countersignature
import * as React from 'react';
import { SortableList } from '@bindshq/stance-react';
interface Step {
id: string;
name: string;
role: string;
}
const approvalSteps: Step[] = [
{ id: 'legal', name: 'Legal review', role: 'Amara Okafor' },
{ id: 'finance', name: 'Finance approval', role: 'Hana Sato' },
{ id: 'director', name: 'Director sign-off', role: 'Lukas Brenner' },
{ id: 'counter', name: 'Countersignature', role: 'Client' },
];
export function Basic() {
const [steps, setSteps] = React.useState(approvalSteps);
return (
<div style={{ width: '100%', maxWidth: 440 }}>
<SortableList
label="Approval order"
items={steps}
getItemId={(s) => s.id}
getItemLabel={(s) => s.name}
renderItem={(s) => s.name}
onReorder={setSteps}
/>
</div>
);
}Light, dark and high contrast
Every Stance component is designed and tested in all three appearance modes.
- Legal review
- Finance approval
- Director sign-off
- Countersignature
- Legal review
- Finance approval
- Director sign-off
- Countersignature
- Legal review
- Finance approval
- Director sign-off
- Countersignature
Import
import { SortableList } from '@bindshq/stance-react';Examples
With move buttons
A single-pointer alternative to dragging, recommended on touch screens.
- Legal review
- Finance approval
- Director sign-off
- Countersignature
import * as React from 'react';
import { SortableList } from '@bindshq/stance-react';
interface Step {
id: string;
name: string;
role: string;
}
const approvalSteps: Step[] = [
{ id: 'legal', name: 'Legal review', role: 'Amara Okafor' },
{ id: 'finance', name: 'Finance approval', role: 'Hana Sato' },
{ id: 'director', name: 'Director sign-off', role: 'Lukas Brenner' },
{ id: 'counter', name: 'Countersignature', role: 'Client' },
];
export function MoveButtons() {
const [steps, setSteps] = React.useState(approvalSteps);
return (
<div style={{ width: '100%', maxWidth: 440 }}>
<SortableList
label="Approval order"
items={steps}
getItemId={(s) => s.id}
getItemLabel={(s) => s.name}
renderItem={(s) => s.name}
onReorder={setSteps}
showMoveButtons
/>
</div>
);
}Rich items
renderItem receives the item and its state, so rows can show anything.
- Step 1Legal reviewAmara Okafor
- Step 2Finance approvalHana Sato
- Step 3Director sign-offLukas Brenner
- Step 4CountersignatureClient
import * as React from 'react';
import { Badge, SortableList } from '@bindshq/stance-react';
interface Step {
id: string;
name: string;
role: string;
}
const approvalSteps: Step[] = [
{ id: 'legal', name: 'Legal review', role: 'Amara Okafor' },
{ id: 'finance', name: 'Finance approval', role: 'Hana Sato' },
{ id: 'director', name: 'Director sign-off', role: 'Lukas Brenner' },
{ id: 'counter', name: 'Countersignature', role: 'Client' },
];
export function RichItems() {
const [steps, setSteps] = React.useState(approvalSteps);
return (
<div style={{ width: '100%', maxWidth: 480 }}>
<SortableList
label="Approval order"
items={steps}
getItemId={(s) => s.id}
getItemLabel={(s) => s.name}
onReorder={setSteps}
renderItem={(s, state) => (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 12,
}}
>
<div>
<div style={{ fontWeight: 600 }}>{s.name}</div>
<div style={{ color: 'var(--st-color-fg-muted)', fontSize: 13 }}>{s.role}</div>
</div>
<Badge tone={state.index === 0 ? 'info' : 'neutral'}>Step {state.index + 1}</Badge>
</div>
)}
/>
</div>
);
}- Legal review
- Finance approval
- Director sign-off
- Countersignature
import { SortableList } from '@bindshq/stance-react';
interface Step {
id: string;
name: string;
role: string;
}
const approvalSteps: Step[] = [
{ id: 'legal', name: 'Legal review', role: 'Amara Okafor' },
{ id: 'finance', name: 'Finance approval', role: 'Hana Sato' },
{ id: 'director', name: 'Director sign-off', role: 'Lukas Brenner' },
{ id: 'counter', name: 'Countersignature', role: 'Client' },
];
export function Disabled() {
return (
<div style={{ width: '100%', maxWidth: 440 }}>
<SortableList
label="Approval order (locked)"
items={approvalSteps}
getItemId={(s) => s.id}
getItemLabel={(s) => s.name}
renderItem={(s) => s.name}
onReorder={() => {}}
disabled
/>
</div>
);
}API reference
| Prop | Type | Default | Description |
|---|---|---|---|
items* required | T[] | None | The items in their current order. |
getItemId* required | (item: T) => string | None | A stable, unique id for each item. |
getItemLabel | (item: T) => string | None | Plain-text name used for handle labels and announcements. Defaults to the id. |
renderItem* required | (item: T, state: SortableItemState) => ReactNode | None | Renders the row content. State has index, dragging and disabled. |
onReorder* required | (items: T[], move: { from: number; to: number }) => void | None | Called with the new order when an item is dropped or moved. |
label* required | string | None | Accessible name of the list. |
showMoveButtons | boolean | false | Show Move up and Move down buttons on each row. |
disabled | boolean | false | Prevents reordering. |
labels | Partial<SortableListLabels> | None | Translations for handle names, instructions and announcements. |
ref | Ref<SortableListHandle> | None | moveItem(from, to) reorders programmatically. |
Accessibility
- Each row has a handle button named after its item, for example “Reorder Legal review”, with the keyboard instructions as its description.
- Keyboard reordering follows the accessible drag-and-drop pattern: pick up, move, drop or cancel. Nothing is committed until the item is dropped.
- Every step is announced: “Picked up Legal review, position 1 of 4”, “moved to position 3 of 4”, “Dropped”, “Cancelled”.
- Move up and Move down buttons give a single-pointer alternative to dragging (WCAG 2.5.7).
- Focus stays on the moved item’s handle, so people can keep adjusting.
- The list is a real list, so screen readers report the number of items and each position.
Keyboard
| Keys | Action |
|---|---|
| Space Enter | Picks up the focused item, or drops it in its new position. |
| ↑ ↓ | Moves the picked-up item one position. |
| Home End | Moves the picked-up item to the start or end. |
| Esc | Cancels and returns the item to where it started. |
| Tab | Drops the item where it is and moves focus on. |
Usage guidelines
Do
- Turn on showMoveButtons on touch-first screens and for long lists.
- Save the new order as soon as it changes, or make the Save action obvious.
Don’t
- Use it for lists that sort themselves (by date or name); offer sorting instead.
- Hide the handle until hover; touch and keyboard users would never find it.