Mosmox UI
Current version
GitHubnpmjs.com
Content & Media

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, default is standard, and lg has 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.
  • fullWidth defaults to true; set it to false when the list should size to its parent or custom CSS.

Keyboard and disabled records

Rows are focusable and support keyboard reordering without a pointer:

  • ArrowUp moves the focused record one position up.
  • ArrowDown moves it one position down.
  • Home moves it to the first position.
  • End moves 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

PropTypeDefaultDescription
itemsSortableListItem<T>[]requiredRecords 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) => ReactNodebuilt-in rendererCustom content for each row. The drag handle and row wrapper remain managed by the component.
headerReactNode—Content rendered above the rows.
footerReactNode—Content rendered below the rows.
emptyStateReactNodeNo itemsContent shown when items is empty.
sizesm | default | lgdefaultRow density and typography scale.
variantplain | divided | bordered | carddividedList surface and separator treatment.
fullWidthbooleantrueMakes the list fill the available width.
dragHandleLabel(item, index) => stringMove item NAccessible label for each drag handle.
classNamestring—Additional class on the root element.
styleCSSProperties—Inline styles on the root element.
aria-* / data-*HTML attributes—Additional attributes forwarded to the root element.

SortableListItem<T>

FieldTypeDefaultDescription
keystringrequiredStable identity used for drag state and persistence.
titleReactNoderequiredMain row label.
descriptionReactNode—Supporting text below the title.
contentReactNode—Additional body content.
leadingReactNode—Icon or visual before the text.
metaReactNode—Compact metadata at the end of the row.
trailingReactNode—Custom trailing content.
disabledbooleanfalsePrevents dragging and keyboard movement for the record.
dataT—Original application data associated with the record.

SortableListReorderContext

FieldTypeDescription
activeKeystringKey of the moved record.
overKeystringKey of the record at the destination position.
fromIndexnumberOriginal zero-based index.
toIndexnumberNew zero-based index.

Type reference

TypeValues
SortableListSizesm, default, lg
SortableListVariantplain, 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.