Sortable
The foundational drag-and-drop reorder primitive — a zero-dependency, accessible engine that makes any collection reorderable: a single vertical/horizontal list, a wrapping 2D grid, or a Kanban board (cross-container). Keyboard drag mode and screen-reader announcements come for free; edge auto-scroll, a touch long-press sensor, a drop indicator, and multi-item drag are built in. A `useSortable` hook wires the same engine into List, Tree, and Table.
@garn/ui/sortableDefault
A single vertical list — whole-item drag, keyboard-operable, with the shared instructions node.
Handle
A list with a grip handle + interactive row content (badge, menu button) — only the handle drags, so the button stays clickable.
Horizontal
Horizontal reorder — a row of chips, ←/→ keyboard.
Board
A Kanban board — Sortable.Group of columns + a Sortable.Overlay; cards move between columns.
Multi Item
Multi-item drag — click to select, then drag one selected item to move the whole set (count badge).
2 selected · drag one to move all
Grid
A wrapping 2D grid (orientation=grid) — closest-center projection, spatial ←/→ ↑/↓ keyboard, drop indicator.
Drop Indicator
The built-in insertion line (dropIndicator) + onDragOver reporting the live target slot.
Drag a page to reorder
Scrollable
A tall list in a fixed-height scroll container — edge auto-scroll drags past the fold; long-press on touch.
Headless
The useSortable hook wired onto a consumer's own <ul>/<li> markup (the List/Tree integration pattern).
- Overview
- Members
- Billing
- Security
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add sortable- Registry
sortable- Source
- sortable.tsx
- Deps
@radix-ui/react-slotlucide-react- Registry deps
utilsuse-controllable-stateuse-announcedrag-gestureuse-isomorphic-layout-effect
Import
import { Sortable } from "@garn/ui/sortable";Props
Generated from the component's TypeScript types.
activationDistance- Description
- Pointer travel (px) before a mouse/pen drag begins — guards clicks.
- Type
number
announcements- Description
- No description.
- Type
Partial<SortableAnnouncements>
asChild- Description
- No description.
- Type
boolean
autoScroll- Description
- Auto-scroll the nearest scroll container when dragging near its edge (default true).
- Type
boolean
collision- Description
- Drop-index projection: `pointer` (1D) or `closest-center` (2D reading order).
- Type
"closest-center" | "pointer"
defaultValue- Description
- Ordered item keys (uncontrolled initial).
- Type
string[]
disabled- Description
- No description.
- Type
boolean
dropIndicator- Description
- Show a built-in insertion line at the drop position (default false).
- Type
boolean
getItemLabel- Description
- Map an item key to the label screen readers announce.
- Type
(value: string) => string
id- Description
- Container id — auto-generated; set it when composing a Board by hand.
- Type
string
lockAxis- Description
- Constrain the self-translate follow to the main axis (default true).
- Type
boolean
onDragCancel- Description
- No description.
- Type
(e: { value: string; }) => void
onDragEnd- Description
- No description.
- Type
(e: ReorderMeta) => void
onDragMove- Description
- Fires for every drag point: rAF-throttled moves, auto-scroll re-projections, and a synchronous final point at the drop.
- Type
(e: DragMoveEvent) => void
onDragOver- Description
- Fires when the projected drop slot (container/index) changes.
- Type
(e: DragOverEvent) => void
onDragStart- Description
- No description.
- Type
(e: { value: string; }) => void
onValueChange- Description
- Fires with the next order and what moved.
- Type
(value: string[], meta: ReorderMeta) => void
orientation- Description
- Layout + axis: `vertical` (default), `horizontal`, or a wrapping 2D `grid`.
- Type
"grid" | "horizontal" | "vertical"
selectedValues- Description
- Selected item values — dragging one moves the whole set (multi-item drag).
- Type
string[]
touchDelay- Description
- Long-press (ms) before a *touch* drag begins — a quick swipe scrolls instead.
- Type
number
value- Description
- Ordered item keys (controlled).
- Type
string[]
Plus 275 inherited native <div> attributes.
Sortable.Item
asChild- Description
- No description.
- Type
boolean
disabled- Description
- Make this item non-draggable (a pinned/locked row).
- Type
boolean
hasHandle- Description
- Declare that a `Sortable.Handle` lives somewhere in this item's rendered output. The automatic detection only walks the JSX you pass as children — a handle rendered *inside* one of your own components is invisible to it, which would silently make the whole item the activator.
- Type
boolean
valueRequired- Description
- The item's stable key — its identity in the ordered `value`.
- Type
string
Plus 280 inherited native HTML attributes.
Sortable.Handle
asChild- Description
- No description.
- Type
boolean
label- Description
- Accessible name for the icon-only handle (default "Drag to reorder").
- Type
string
Plus 290 inherited native HTML attributes.
Sortable.Overlay
childrenRequired- Description
- Render the ghost for the active key (clip-free; follows the pointer).
- Type
(activeValue: string) => React.ReactNode
className- Description
- No description.
- Type
string
Sortable.Group
activationDistance- Description
- Pointer travel (px) before a mouse/pen drag begins.
- Type
number
announcements- Description
- No description.
- Type
Partial<SortableAnnouncements>
asChild- Description
- No description.
- Type
boolean
autoScroll- Description
- Auto-scroll the nearest scroll container near an edge (default true).
- Type
boolean
collision- Description
- Drop-index projection: `pointer` (1D) or `closest-center` (2D reading order).
- Type
"closest-center" | "pointer"
disabled- Description
- No description.
- Type
boolean
dropIndicator- Description
- Show a built-in insertion line at the drop position (default false).
- Type
boolean
getItemLabel- Description
- No description.
- Type
(value: string) => string
lockAxis- Description
- Constrain the self-translate follow to the main axis (default true).
- Type
boolean
onDragCancel- Description
- No description.
- Type
(e: { value: string; }) => void
onDragEnd- Description
- No description.
- Type
(e: ReorderMeta) => void
onDragMove- Description
- Fires for every drag point: rAF-throttled moves, auto-scroll re-projections, and a synchronous final point at the drop.
- Type
(e: DragMoveEvent) => void
onDragOver- Description
- Fires when the projected drop slot (container/index) changes.
- Type
(e: DragOverEvent) => void
onDragStart- Description
- No description.
- Type
(e: { value: string; }) => void
orientation- Description
- Layout + axis for the columns' items: `vertical` (default), `horizontal`, or `grid`.
- Type
"grid" | "horizontal" | "vertical"
selectedValues- Description
- Selected item values — dragging one moves the whole set (multi-item drag).
- Type
string[]
touchDelay- Description
- Long-press (ms) before a touch drag begins.
- Type
number
Plus 277 inherited native HTML attributes.
Styling
Target these data-slots and remap these tokens to restyle without forking the component.
[data-slot="sortable"]| Property | Token | Tier |
|---|---|---|
itemGap | --garn-gap-stack | semantic · air |
columnGap | --garn-gap-inline | semantic · air |
motion | --garn-motion-fast | semantic · air |
easing | --garn-ease-standard | semantic |
indicator | --garn-brand-solid | semantic |
Overview
Sortable is garn's foundational drag-and-drop — the accessible reorder engine everything else composes. It has zero runtime dependency: a Pointer-Events + keyboard machine that ships in the registry as source. Reach for it to make a collection reorderable — a single vertical or horizontal list, a wrapping 2D grid, or a Kanban board where items move between columns. A Sortable wraps items keyed by value; drag by the whole item, or add a Sortable.Handle so interactive row content stays clickable. It is accessible by default — a full keyboard drag mode (lift → move → drop / cancel) and live-region announcements come for free, not as an opt-in. Consumers that already own their rows (List's reorderable, Tree's drag-reparent) wire in the same engine through the headless useSortable hook.
Guidelines
One Sortable = one list; a Sortable.Group = a Board. A standalone Sortable owns its ordered value and the drag machine; wrapping several in a Sortable.Group hoists one machine across columns so items move between them (add a Sortable.Overlay so the ghost escapes each column's clipping).
Drag the whole item, or add a handle. A bare Sortable.Item is the activator — good for simple rows. When the row has buttons/links/inputs, add a Sortable.Handle so only the grip drags; on semantic <li>/<tr>/treeitem rows a handle is required (a role=button on the row breaks their semantics).
Own your data. Reorder is controlled via value/onValueChange (or uncontrolled defaultValue); apply the reported order to your state. In a Board, each column owns its own value.
Reach for the built-ins before a recipe. orientation="grid" for 2D; selectedValues for multi-item drag; dropIndicator for an insertion line; autoScroll (on by default) for long lists; touchDelay long-press for mobile. The keyboard describedby wires itself up — the root injects the shared instructions node on mount, so there's nothing to render for it; just don't strip the focus ring or silence the announcements.
Best practices
- Control order with
value/onValueChange(ordefaultValue) and apply the reported order to your data. - Add a
Sortable.Handlewhen the row has interactive content, and always on semantic <li>/<tr>/treeitem rows. - Override the four screen-reader messages via
announcementsfor domain language (e.g. "Moved to Done") — the shared keyboard-instructions node is injected by the root, so there's nothing to render for the describedby. - Use a
Sortable.Group+Sortable.Overlayfor a Board;orientation="grid"for a 2D grid;selectedValuesfor multi-item drag. - Leave
autoScrollon for long lists and settouchDelay(long-press) for touch — a swipe should still scroll.
- Don't put
role="button"on a semantic <li>/<tr>/treeitem to make it draggable — it breaks list/grid/tree semantics; drag via a Sortable.Handle instead. - Don't build a Board without a Sortable.Overlay — the dragged card gets clipped by a column's overflow.
- Don't lock touch scrolling — the whole-item touch sensor is long-press so the list still scrolls; don't set touch-action:none on rows yourself.
- Don't silence the live-region announcements or remove the focus ring — the keyboard drag model depends on both.
Accessibility
- ARIA APG
- Drag-and-Drop (keyboard-operable reorder)
- Focus
- The drag activator (a Sortable.Handle, or the bare Sortable.Item) is a focusable button; focus stays on the moved item after a keyboard drop or cancel. Don't strip the focus ring.
Enforceable contracts
The drag activator must have an accessible name (Sortable.Handle defaults to "Drag to reorder"; override via `label`, or name the item).
when a Sortable.Handle (or a bare-item activator) is icon-only
No consumer markup is needed: the Sortable root injects one shared visually-hidden keyboard-instructions node (id `garn-sortable-instructions`) into document.body on mount — idempotent by id — and every activator's aria-describedby points at it. Don't remove that node or hand-render a duplicate; to change the spoken text, override `announcements`.
when any Sortable (or the useSortable hook) is rendered
Keyboard
| SpaceEnter | On the drag activator: pick the item up; press again to drop it at the chosen position. |
| ArrowUpArrowDown | Move the lifted item (main axis of a vertical list / a whole row in a grid / between columns in a horizontal Board). |
| ArrowLeftArrowRight | Move the lifted item along the horizontal axis / one cell in a grid / between Board columns (RTL-aware). |
| HomeEnd | Send the lifted item to the first / last position. |
| Escape | Cancel the drag and restore the item to its original position. Works for pointer drags too — a window-level listener for the drag's duration, so it cancels even when the activator isn't focused (Safari doesn't focus buttons on mousedown). |
- • The drag activator — a dedicated Sortable.Handle button, or the Sortable.Item itself when it has no handle — is a real button with an accessible name, aria-roledescription="sortable", and aria-describedby pointing at a shared visually-hidden keyboard-instructions node that the Sortable root injects into document.body on mount (id `garn-sortable-instructions`, idempotent by id) — consumers render nothing for it. Composing a handle keeps semantic <li>/<tr>/treeitem rows intact (a role=button on the row would break their list/grid/tree semantics), so headless integrations (List, Tree) drag via a handle.
- • Keyboard drag mode: Space or Enter on the activator lifts the item; the arrow keys move it (↑/↓ for a vertical list, ←/→ for horizontal, the cross-axis hops between columns in a Board, spatial in a grid); Home/End jump to the first/last position; Space or Enter drops and commits; Escape cancels and restores. Focus follows the item throughout.
- • A polite aria-live region announces pick-up, each position change, drop, and cancel ("Picked up X. Position 2 of 5." … "Dropped X. Position 3 of 5."). The four messages are overridable via `announcements` for domain language (e.g. "Moved to Done"). Multi-item drags announce the count.
- • Motion is not required to operate: the dragged item follows the pointer with no transition (the drag itself), sibling gaps smooth under `--garn-motion-fast` and collapse to instant under prefers-reduced-motion; on touch a long-press (default 140ms) starts a drag so a swipe still scrolls the list.
- Status
- Stable
- Version
v0.3.0- Introduced
v0.2.0
garn doesn't yet record a per-release version history. This reflects the component's current lifecycle state; full changelog entries will appear once the metadata pipeline emits a lifecycle.history field.