Sortable List
A full-width list whose records can be reordered with drag and drop or the keyboard.
SortableList renders only the records supplied by the application. It does not paginate, fetch data, or persist changes itself. When a user moves a record, onReorder receives the complete new array and a context object; send that array to your API and update the items prop with the saved order.
Basic usage
import { SortableList } from '@mosmox/ui';
const items = [
{ key: 'research', title: 'Research', description: 'Discovery and product insights' },
{ key: 'design', title: 'Design', description: 'Flows and visual language' },
{ key: 'engineering', title: 'Engineering', description: 'Implementation and delivery' },
];
<SortableList
items={items}
onReorder={(nextItems) => saveOrder(nextItems.map((item) => item.key))}
/>The list is full width by default. The component updates its visible order immediately, while the parent should persist the new sequence and pass the saved array back through items.
Persisting the order on a server
The callback is the integration boundary. It receives the entire ordered record array, so the server can replace the persisted order in one request. The callback also receives activeKey, overKey, fromIndex, and toIndex for audit logging or an incremental API.
const [items, setItems] = useState(initialItems);
const [saving, setSaving] = useState(false);
async function handleReorder(nextItems, context) {
setItems(nextItems); // optimistic UI
setSaving(true);
try {
const response = await fetch('/api/projects/order', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: nextItems.map((item, index) => ({ id: item.key, position: index })),
moved: context,
}),
});
if (!response.ok) throw new Error('Could not save the order');
} catch {
setItems(initialItems); // restore the last confirmed order
} finally {
setSaving(false);
}
}
<SortableList
items={items}
onReorder={handleReorder}
footer={saving ? 'Saving order…' : 'Order saved'}
/>SortableList does not make the request itself. This keeps authentication, API shape, retries, optimistic updates, and error handling in the application layer.
Record shape and custom rendering
Each record requires a stable string key and a title. description, content, leading, meta, trailing, disabled, and application-specific data are optional. The default renderer covers common list rows; use renderItem when the row needs custom content.
<SortableList
items={items}
renderItem={(item, state) => (
<>
<span>{state.index + 1}</span>
<strong>{item.title}</strong>
{state.dragging && <Tag tone="primary">Moving</Tag>}
</>
)}
onReorder={(nextItems) => saveOrder(nextItems)}
/>The custom renderer supplies index, dragging, over, and disabled. The drag handle and row semantics remain provided by SortableList.
Sizes, variants, and layout
<SortableList items={items} size="sm" variant="plain" />
<SortableList items={items} size="default" variant="divided" />
<SortableList items={items} size="lg" variant="card" />
<SortableList items={items} variant="bordered" fullWidth />size="sm"is compact,defaultis standard, andlghas more generous row spacing and typography.variant="plain"uses transparent rows with separators.variant="divided"groups rows inside a bordered container with separators.variant="bordered"uses the same contained structure for a clear list boundary.variant="card"adds a surface, border radius, and shadow around the list.fullWidthdefaults totrue; set it tofalsewhen the list should size to its parent or custom CSS.
Keyboard and disabled records
Rows are focusable and support keyboard reordering without a pointer:
ArrowUpmoves the focused record one position up.ArrowDownmoves it one position down.Homemoves it to the first position.Endmoves it to the last position.
Set disabled: true on a record to prevent it from being dragged or moved into. The drag handle is disabled as well. Each row exposes role="listitem", aria-posinset, and aria-setsize; the handle has an accessible label generated from its position or supplied through dragHandleLabel.
Empty state and surrounding content
<SortableList
items={[]}
emptyState="There are no workflow steps yet."
header="Workflow steps"
footer="Add a step to begin."
/>header and footer accept any React content. There is no pagination control or built-in data loading.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
items | SortableListItem<T>[] | required | Records rendered in the current order. Each record needs a stable string key and title. |
onReorder | (items, context) => void | — | Called after a successful drag or keyboard move with the complete new order and reorder metadata. |
renderItem | (item, state) => ReactNode | built-in renderer | Custom content for each row. The drag handle and row wrapper remain managed by the component. |
header | ReactNode | — | Content rendered above the rows. |
footer | ReactNode | — | Content rendered below the rows. |
emptyState | ReactNode | No items | Content shown when items is empty. |
size | sm | default | lg | default | Row density and typography scale. |
variant | plain | divided | bordered | card | divided | List surface and separator treatment. |
fullWidth | boolean | true | Makes the list fill the available width. |
dragHandleLabel | (item, index) => string | Move item N | Accessible label for each drag handle. |
className | string | — | Additional class on the root element. |
style | CSSProperties | — | Inline styles on the root element. |
aria-* / data-* | HTML attributes | — | Additional attributes forwarded to the root element. |
SortableListItem<T>
| Field | Type | Default | Description |
|---|---|---|---|
key | string | required | Stable identity used for drag state and persistence. |
title | ReactNode | required | Main row label. |
description | ReactNode | — | Supporting text below the title. |
content | ReactNode | — | Additional body content. |
leading | ReactNode | — | Icon or visual before the text. |
meta | ReactNode | — | Compact metadata at the end of the row. |
trailing | ReactNode | — | Custom trailing content. |
disabled | boolean | false | Prevents dragging and keyboard movement for the record. |
data | T | — | Original application data associated with the record. |
SortableListReorderContext
| Field | Type | Description |
|---|---|---|
activeKey | string | Key of the moved record. |
overKey | string | Key of the record at the destination position. |
fromIndex | number | Original zero-based index. |
toIndex | number | New zero-based index. |
Type reference
| Type | Values |
|---|---|
SortableListSize | sm, default, lg |
SortableListVariant | plain, divided, bordered, card |
SortableListRenderState | { index, dragging, over, disabled } |
The component has no pagination, sorting query, server request, or persistence side effect. The application owns the records and decides how onReorder is translated into a server request.