# SortableList

> A list people can reorder by dragging, with the keyboard, or with move buttons, announcing every change.

Category: Advanced · Status: stable · Package: @bindshq/stance-react@0.1.1 · Docs: https://stanceui.com/components/sortable-list

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.

## Import

```tsx
import { SortableList } from '@bindshq/stance-react';
```

Styles are imported once per app: `@bindshq/stance-tokens/css` and `@bindshq/stance-react/styles.css`.

## Examples

### Approval order

```tsx
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>
  );
}
```

### With move buttons

A single-pointer alternative to dragging, recommended on touch screens.

```tsx
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.

```tsx
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>
  );
}
```

### Disabled

```tsx
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

| Component | Prop | Type | Default | Description |
| --- | --- | --- | --- | --- |
| SortableList | `items` (required) | `T[]` | None | The items in their current order. |
| SortableList | `getItemId` (required) | `(item: T) => string` | None | A stable, unique id for each item. |
| SortableList | `getItemLabel` | `(item: T) => string` | None | Plain-text name used for handle labels and announcements. Defaults to the id. |
| SortableList | `renderItem` (required) | `(item: T, state: SortableItemState) => ReactNode` | None | Renders the row content. State has index, dragging and disabled. |
| SortableList | `onReorder` (required) | `(items: T[], move: { from: number; to: number }) => void` | None | Called with the new order when an item is dropped or moved. |
| SortableList | `label` (required) | `string` | None | Accessible name of the list. |
| SortableList | `showMoveButtons` | `boolean` | `false` | Show Move up and Move down buttons on each row. |
| SortableList | `disabled` | `boolean` | `false` | Prevents reordering. |
| SortableList | `labels` | `Partial<SortableListLabels>` | None | Translations for handle names, instructions and announcements. |
| SortableList | `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. |

## 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.

## Related

- https://stanceui.com/components/list
- https://stanceui.com/components/data-table
- https://stanceui.com/components/stepper
