Drag-and-Drop List

Free

Native HTML5 drag-and-drop reorderable list with priority badges, keyboard move alternatives and item removal.

</>sv-dnd-list dndreorderlist

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 demo

More examples

Sortable with item click

example.htmlAngular
<sv-dnd-list [items]="items" [sortable]="true" [clickable]="true" (reordered)="save($event)" (itemClick)="open($event)" />

Handle-only drag

example.htmlAngular
<sv-dnd-list [items]="items" [useHandle]="true" (reordered)="save($event)" />

Persisting a reorder

example.htmlAngular
<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 error to show it in place — without taking away what the user was working on.

Inputs

NameTypeDefaultDescription
itemsDndItem[] (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.
variantDndListVariant'default'Visual variant.
labelstring''Accessible label for the list.
sortablebooleantrueEnable drag-and-drop reordering.
savingbooleanfalseTrue while the new order is being saved. Dragging is blocked, so a second reorder cannot race the first and leave `rollback()` ambiguous.
errorSvDataError | string | nullnullFailure saving the order. Rendered above the list; the rows stay put so the user can see what state things are in.
emptyTextstring'Nothing to order yet'Copy for the empty state.
loadingbooleanfalseTrue while the list is being fetched. A list whose order you persist is a list you fetched first.
useHandlebooleanfalseRestrict drag initiation to the grip handle only (rows can't be dragged by their body). The handle "arms" the row on pointer-down.
clickablebooleanfalseRender each item's label as a button that emits `itemClick` — for a list whose items open something (a detail panel, a task).

Outputs

EventPayload typeDescription
retryvoidThe 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.
reorderedDndReorderEventThe 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.
itemRemovedDndRemoveEventA 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.
itemChangedDndItemChangeEventA row was ticked complete or moved to another column. Applied immediately; `rollback()` undoes it.
itemClickDndItemEmitted with the item whose label was activated (click, Enter or Space). Needs `clickable`.
itemsChangeDndItem[]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.

usage.htmlAngular
<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.

host.component.tsTypeScript
// 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.

Loads giscus.app and GitHub — nothing is fetched until you click.