Drag-and-Drop List
FreeNative HTML5 drag-and-drop reorderable list with priority badges, keyboard move alternatives and item removal.
Complexity tier: Interactive The primitive bar, plus declared async states, a keyboard model, focus restoration and undo for destructive actions.
import { SvDndListComponent } from '@styloviz/display';Demo & usage
Basic usage
Usage example — open the gallery for the full interactive demo.
An interactive demo for this component lives in the gallery.
Open live demoMore examples
Sortable with item click
<sv-dnd-list [items]="items" [sortable]="true" [clickable]="true" (reordered)="save($event)" (itemClick)="open($event)" />
Handle-only drag
<sv-dnd-list [items]="items" [useHandle]="true" (reordered)="save($event)" />
Persisting a reorder
<sv-dnd-list [(items)]="items" [saving]="saving()" [error]="error()" (reordered)="onReorder($event)" /> // The list reorders before the server has heard about it, so it hands back the // only copy of the previous order. Bind [saving] so a second drag cannot start // while the first is in flight — that is what keeps rollback() unambiguous. onReorder(event: DndReorderEvent): void { this.saving.set(true); this.api.saveOrder(event.items.map(i => i.id), event.requestKey).subscribe({ next: () => this.saving.set(false), error: () => { event.rollback(); this.error.set('Could not save the new order.'); this.saving.set(false); }, }); }
When to use
- Letting users manually order a short-to-medium list (priorities, steps, playlists).
- You want native drag-and-drop with keyboard move alternatives.
- For moving items between two lists, use Transfer; for cross-column flow, Kanban.
Async states
- loading
- empty
- error
- retry
Server writes
- Writes
- reordered
- itemRemoved
- itemChanged
Each payload carries a
requestKey— send it with the request so a retry cannot apply the same change twice.- Rollback
- Call
event.rollback()when the save fails — the change is already on screen, and this is what puts it back. - While saving
- Bind
saving— with two writes outstanding there is no single state to roll back to, so the action stays locked until the first one lands. - On failure
- Pass the failure to
errorto show it in place — without taking away what the user was working on.
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
| items | DndItem[] (two-way) | [] | The items to render and reorder. Two-way — this is a `model()`. The items to order. Defaults to empty — this used to default to a demo fixture, so an unbound list rendered convincing fake rows. |
| variant | DndListVariant | 'default' | Visual variant. |
| label | string | '' | Accessible label for the list. |
| sortable | boolean | true | Enable drag-and-drop reordering. |
| saving | boolean | false | True while the new order is being saved. Dragging is blocked, so a second reorder cannot race the first and leave `rollback()` ambiguous. |
| error | SvDataError | string | null | null | Failure saving the order. Rendered above the list; the rows stay put so the user can see what state things are in. |
| emptyText | string | 'Nothing to order yet' | Copy for the empty state. |
| loading | boolean | false | True while the list is being fetched. A list whose order you persist is a list you fetched first. |
| useHandle | boolean | false | Restrict drag initiation to the grip handle only (rows can't be dragged by their body). The handle "arms" the row on pointer-down. |
| clickable | boolean | false | Render each item's label as a button that emits `itemClick` — for a list whose items open something (a detail panel, a task). |
Outputs
| Event | Payload type | Description |
|---|---|---|
| retry | void | The host should re-run whatever failed — the initial load, or the save behind the last reorder. One output covers both because from the user's side there is one button, and which request it re-runs is the host's business, not theirs. |
| reordered | DndReorderEvent | The order changed. The move is applied immediately — a row that snaps back while a request is in flight feels broken — so the payload carries `rollback()` for when the save fails. It refuses, with a dev-mode warning, once a later reorder has happened; bind `saving` so that cannot arise. |
| itemRemoved | DndRemoveEvent | A row was deleted. Applied immediately; `rollback()` puts it back. Bind this or the delete never happens — the list on screen is the component's, and it has no way to reach your API. |
| itemChanged | DndItemChangeEvent | A row was ticked complete or moved to another column. Applied immediately; `rollback()` undoes it. |
| itemClick | DndItem | Emitted with the item whose label was activated (click, Enter or Space). Needs `clickable`. |
| itemsChange | DndItem[] | Emits the new `items` whenever it changes — the output half of `[(items)]`. |
Wire it to your API
This component is headless about transport: it renders what you give it and reports what the user asks for. Nothing below requires @styloviz/data — drive it from whatever you already use.
Template
Bound to the names this component actually exposes.
<sv-dnd-list [items]="data.items()" [loading]="data.loading()" [error]="data.error()" (retry)="data.reload()" />
Host class
Drive the three async inputs from your own fetch.
// This component reports its state but does not emit a query, so drive // it from your own fetch and bind the three async inputs. readonly items = signal<Row[]>([]); readonly loading = signal(false); readonly error = signal<SvDataError | null>(null); async load(): Promise<void> { this.loading.set(true); this.error.set(null); try { this.items.set(await this.api.fetchRows()); } catch (e) { this.error.set(toSvDataError(e)); } finally { this.loading.set(false); } }
- This component does not emit a query, so it renders what you give it. Fetch on your own trigger and bind `items`, `loading` and `error`.
- Normalize failures with `toSvDataError()` from `@styloviz/core`. It derives `retryable` from the status, so a 404 does not get a "Try again" button that can only fail again.
Full walkthrough: Connecting to a backend.
Customization & theming
- Native drag-and-drop reordering.
- Set `useHandle` to restrict dragging to the grip handle only (rows can't be dragged by their body).
- Per-item badges and remove control.
- Custom item templates.
- All three writes — `(reordered)`, `(itemRemoved)`, `(itemChanged)` — carry `requestKey` and `rollback()`, because each is applied before the server has heard about it.
- `saving` locks dragging and the row controls while a write is in flight; `error` reports the failure above the list.
- The `kanban` variant stacks to a single column on phones (`grid-cols-1 sm:grid-cols-3`) so cards stay readable, then sits three-across from `sm` up.
Best practices
- Do: Persist the new order on `reordered` and reflect it optimistically — the list already has.
- Do: Bind `saving` while a write is in flight, and call `event.rollback()` when it fails. The list holds the only copy of the previous order.
- Do: Send `event.requestKey` and ignore a key you have already applied, so a retried save cannot land twice.
- Do: Keep keyboard move-up/down available — not everyone can drag.
- Don't: Don't use manual reordering for long lists where sorting rules would be better.
- Don't: Don't hold a `rollback()` handle past the next reorder — it is refused, with a dev-mode warning, because restoring that snapshot would discard the newer move too.
Accessibility
- In the default, checklist and compact variants every row has a real grip button (`data-dnd-handle`) for keyboard grab/drop: Space or Enter lifts, the arrow keys move, Space drops (one `reordered` for the whole move) and Escape cancels and restores the original order.
- Labelled Move up / Move down buttons in those same variants commit one step at a time — unaffected by `useHandle`. The kanban variant moves cards between columns with named Move left / Move right buttons that are revealed on keyboard focus as well as hover.
- After a removal focus moves to the row that took its place (or the previous row, or the list itself when it is empty) instead of falling to <body>.
- A polite live region announces the new position after every reorder, removal and complete toggle.
Feedback on Drag-and-Drop List
Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.