Sortable
sortableReorders items through pointer and keyboard drag interactions.
Usage
Column settings · Handle drag (vertical)
Controlled items + onChange takes back the new sequence; when handle is used, only the left handle can be dragged and the keyboard can be reached.
Drag to adjust column order (grab Space after handle dragging/focus on the handle·Move with direction keys·Put down Space)
- Order numberUnique identifier
- Customer NameFrom customer master data
- Order amountTax included
- Order statusEnumeration
- Person in chargeCurrent follower
- Creation timeSortable
Current order:Order number → Customer Name → Order amount → Order status → Person in charge → Creation time
const [fields, setFields] = useState(initialFields);
<Sortable
items={fields}
onChange={setFields}
handle
renderItem={(f) => (
<div className="flex items-center justify-between gap-3">
<span className="font-medium text-foreground">{f.label}</span>
<span className="shrink-0 text-xs text-muted">{f.hint}</span>
</div>
)}
/>The entire item can be dragged (no handle)
When handle={false}, the entire row can be dragged, which is suitable for simple lists with no interactive elements in the row.
Drag to adjust column order (grab Space after handle dragging/focus on the handle·Move with direction keys·Put down Space)
- Order numberUnique identifier
- Customer NameFrom customer master data
- Order amountTax included
- Order statusEnumeration
- Person in chargeCurrent follower
- Creation timeSortable
Current order:Order number → Customer Name → Order amount → Order status → Person in charge → Creation time
const [fields, setFields] = useState(initialFields);
<Sortable
items={fields}
onChange={setFields}
renderItem={(f) => <span className="font-medium text-foreground">{f.label}</span>}
/>Inline interactive element + serial number (state.index)
When the entire item can be dragged, input/button in the line will not be hijacked by dragging (the guard is built in the sensor layer, no need to set handle); state.index directly gives the subscript, which is used for "Question N" and the unique aria-label.
There are input boxes and buttons in the row: dragging them will not trigger sorting, dragging the blank space will sort it.
- No. 1 QuestionYour overall satisfaction with this service
- No. 2 QuestionIs the door-to-door staff on time?
- No. 3 QuestionIs the problem solved at once?
<Sortable
items={list}
onChange={setList}
renderItem={(q, { index }) => (
<div className="flex items-center gap-2">
<span className="w-14 shrink-0 text-xs text-muted">Question {index + 1}</span>
<span className="min-w-0 flex-1 truncate">{q.title}</span>
{/* The input box can be dragged to select text and the button can be clicked, but it will not trigger sorting */}
<input type="number" value={q.score} aria-label={`${index + 1} question score`} onChange={...} />
<button type="button" aria-label={`Delete question ${index + 1}`} onClick={...}>Delete</button>
</div>
)}
/>Horizontal sorting (orientation)
orientation="horizontal" arranged horizontally, suitable for Kanban columns/filter tags.
Horizontal drag and drop sorting (kanban column/filter label)
- Pending
- Ongoing
- Completed
- Archived
const [tags, setTags] = useState(initialTags);
<Sortable
items={tags}
orientation="horizontal"
onChange={setTags}
renderItem={(t) => <span className="font-medium text-foreground">{t.name}</span>}
/>When to use
Use Sortable to reorder one list of columns, tags, or form fields. Use Kanban for cross-column movement or Flow for a connected node canvas.
Import
import { Sortable } from "@hulianui/ui"Props
SortableProps<T> is generic.
| Name | Type | Default | Description |
|---|---|---|---|
| items * | T[] | — | Controlled array; write the reordered array from onChange back to state. |
| getId | (item: T) => UniqueIdentifier | Reads item.id | Returns a stable unique id. |
| orientation | "vertical"|"horizontal" | "vertical" | List direction. |
| handle | boolean | false | Restricts dragging to a visible handle; otherwise the whole noninteractive item is draggable. |
| className | string | — | Container class name. |
Events
| Event | Type | Description |
|---|---|---|
| onChange * | (items: T[]) => void | Returns the array after drag or keyboard movement. |
Slots
| Slot | Type | Description |
|---|---|---|
| renderItem * | (item: T, state: SortableItemState) => ReactNode | Renders an item with dragging and zero-based index. |
SortableItemState:
| Field | Type | Description |
|---|---|---|
| dragging | boolean | Whether this item is currently dragged. |
| index | number | Current zero-based index, useful for numbering and unique labels without an O(n²) lookup. |
Usage notes
- The component is controlled;
onChangedoes not mutate state. - IDs must be stable and unique. Array indices break identity after reordering.
- Inputs, controls, links, and contenteditable descendants are guarded from pointer dragging even when
handle={false}; see [[dnd-kit-draggable-container-guard-interactive-children]]. Adddata-no-dragto custom interactive canvases. - The guard stops at the item element and will not lock the list because an outer anchor or label exists.
- Use
state.indexinstead of repeatedly callingitems.findIndex. - Drag-handle accessible labels follow
ConfigProvider locale;enUSprovides “Reorder item N”, while the no-provider fallback remains Chinese.
Related
Table · Book3D · ProTable · PricingTable · JsonViewer · EditableTable
Playground
Drag to adjust column order (grab Space after handle dragging/focus on the handle·Move with direction keys·Put down Space)
- Order numberUnique identifier
- Customer NameFrom customer master data
- Order amountTax included
- Order statusEnumeration
- Person in chargeCurrent follower
- Creation timeSortable
Current order:Order number → Customer Name → Order amount → Order status → Person in charge → Creation time
const [items, setItems] = useState(fields);
<Sortable
items={items}
onChange={setItems}
handle={true}
renderItem={(f) => <span>{f.label}</span>}
/>