Draggable
An accessible drag-and-drop sortable component with pointer and keyboard support, FLIP animations, and single-list or multi-container layouts.
Installation
npx vayu-ui-cli@latest add draggableUsage
List — Drag or keyboard (Space → Arrow ↑↓ → Space)
Inbox
12 unread
Photos
3,429 items
Documents
156 files
Music
2,847 tracks
Videos
89 clips
Archives
24 files
Grid — Drag or keyboard (Space → Arrow ←→↑↓ → Space)
Inbox
Photos
Docs
Music
Videos
Archives
Starred
Favorites
Quick
Cross-list — Drag items between columns
To Do (3)
Done (2)
<Draggable items={items.map((i) => i.id)} onReorder={handleReorder}>
<Draggable.Container layout="list">
{items.map((item) => (
<Draggable.Item key={item.id} value={item.id}>
<div className="flex items-center gap-3 p-3 bg-surface border border-border rounded-surface">
<Draggable.Handle />
<span className="text-sm text-surface-content">{item.title}</span>
</div>
</Draggable.Item>
))}
</Draggable.Container>
<Draggable.Preview />
<Draggable.DropIndicator />
</Draggable>
<Draggable items={items.map((i) => i.id)} onReorder={handleReorder}>
<Draggable.Container layout="grid" columns={3}>
{items.map((item) => (
<Draggable.Item key={item.id} value={item.id}>
<div className="flex flex-col items-center gap-2 p-4 bg-surface border border-border rounded-surface">
<Draggable.Handle />
<span className="text-xs text-surface-content">{item.title}</span>
</div>
</Draggable.Item>
))}
</Draggable.Container>
<Draggable.Preview />
<Draggable.DropIndicator />
</Draggable>
<Draggable containers={containers} onContainersChange={setContainers}>
<div className="grid grid-cols-2 gap-6">
<Draggable.Container containerId="todo" layout="list" aria-label="To Do items">
{containers.todo.map((id) => (
<Draggable.Item key={id} value={id}>
<div className="flex items-center gap-3 p-3 bg-surface rounded-surface border border-border">
<Draggable.Handle />
<span className="text-sm text-surface-content">{taskMap[id]}</span>
</div>
</Draggable.Item>
))}
</Draggable.Container>
<Draggable.Container containerId="done" layout="list" aria-label="Done items">
{containers.done.map((id) => (
<Draggable.Item key={id} value={id}>
<div className="flex items-center gap-3 p-3 bg-surface rounded-surface border border-border">
<Draggable.Handle />
<span className="text-sm text-surface-content">{taskMap[id]}</span>
</div>
</Draggable.Item>
))}
</Draggable.Container>
</div>
<Draggable.Preview />
<Draggable.DropIndicator />
</Draggable>Anatomy
import { Draggable } from 'vayu-ui';
<Draggable items={['a', 'b', 'c']} onReorder={(items) => console.log(items)}>
<Draggable.Container layout="list">
<Draggable.Item value="a">
<Draggable.Handle />
<span>Item A</span>
</Draggable.Item>
<Draggable.Item value="b">
<Draggable.Handle />
<span>Item B</span>
</Draggable.Item>
</Draggable.Container>
<Draggable.Preview />
<Draggable.Placeholder />
<Draggable.DropIndicator />
</Draggable>;- Draggable — Root component. Manages drag state, item ordering, FLIP animations, and provides context to children.
- Draggable.Container — Layout wrapper. Supports
listandgridlayouts with ARIA role and container registration. - Draggable.Item — Sortable item. Handles pointer and keyboard drag initiation, ARIA attributes, and roving tabindex focus.
- Draggable.Handle — Dedicated drag handle with grip icon. Restricts drag initiation to the handle area when present.
- Draggable.Preview — Cursor-following clone of the active item rendered in a portal during pointer drag.
- Draggable.Placeholder — Semi-transparent marker showing the drop position during drag.
- Draggable.DropIndicator — Brand-colored line indicating the exact insertion point during drag.
Accessibility
- Keyboard Support:
Space/Enter— Grab the focused item. Press again to drop.ArrowUp/ArrowDown— Move grabbed item up or down (list), or navigate focus when not dragging.ArrowLeft/ArrowRight— Move grabbed item left or right (grid), or navigate focus when not dragging.Escape— Cancel the active drag operation and return the item to its original position.
- ARIA Attributes:
Draggable.Containerusesrole="list"(list layout) orrole="grid"(grid layout).Draggable.Itemusesrole="listitem"(list) orrole="gridcell"(grid).aria-grabbedonDraggable.Itemreflects whether the item is currently being dragged.aria-roledescription="sortable item"onDraggable.Itemdescribes the interactive role.aria-disabledonDraggable.Itemwhen thedisabledprop is set.aria-describedbylinks items to hidden instructions explaining keyboard controls.aria-labelonDraggable.ContainerandDraggable.Handleprovides accessible labels.
- Focus Behavior:
- Roving tabindex pattern — only the focused item has
tabIndex={0}, all others havetabIndex={-1}. focus-visible:ring-2 focus-visible:ring-focusring provides visible focus indication.- During keyboard drag, the active item receives a brand-colored ring and elevated shadow.
- Roving tabindex pattern — only the focused item has
Screen reader behavior
When a user focuses a Draggable.Item, the screen reader announces the item content and its roledescription as a sortable item. A hidden instruction element (via aria-describedby) announces the available keyboard commands. When the user presses Space or Enter to grab an item, an aria-live="assertive" region announces "Picked up item. Use arrow keys to move." As the item is moved with arrow keys, the live region announces the new position. When dropped, it announces "Dropped at position." If the drag is cancelled with Escape, it announces "Reorder cancelled." In multi-container setups, cross-container moves announce the target container name.
Component Folder Structure
Draggable/
├── Draggable.tsx # Root component with drag state management and FLIP animations
├── DraggableContainer.tsx # Layout container with list/grid support and ARIA roles
├── DraggableItem.tsx # Sortable item with pointer/keyboard drag and focus management
├── DraggableHandle.tsx # Dedicated drag handle with grip icon
├── DraggablePlaceholder.tsx # Drop position marker during drag
├── DraggableDropIndicator.tsx # Insertion point indicator line
├── DraggablePreview.tsx # Cursor-following preview clone during pointer drag
├── types.ts # TypeScript type definitions and utility functions
├── hooks.ts # Context access hooks
└── index.ts # Re-exports with compound component assemblyProps
Draggable (Root)
| Prop | Type | Default | Description |
|---|---|---|---|
items | string[] | — | Controlled array of item IDs for single-list mode. |
defaultItems | string[] | [] | Initial item IDs for uncontrolled single-list mode. |
onReorder | (items: string[]) => void | — | Callback after single-list reorder. |
containers | ContainersMap | — | Controlled multi-container state for cross-list dragging. |
defaultContainers | ContainersMap | {} | Initial multi-container state for uncontrolled mode. |
onContainersChange | (containers: ContainersMap) => void | — | Callback after multi-container reorder. |
children | ReactNode | — | Draggable.Container with items and optional overlays. |
className | string | — | Additional CSS classes. |
Draggable.Container
| Prop | Type | Default | Description |
|---|---|---|---|
layout | "list" | "grid" | "list" | Layout mode for items. |
columns | number | 3 | Number of grid columns (grid mode only). |
containerId | string | — | Unique ID for multi-container mode. |
aria-label | string | "Sortable list" | Accessible label for the container. |
children | ReactNode | — | Draggable.Item elements. |
className | string | — | Additional CSS classes. |
Draggable.Item
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Unique identifier matching an entry in items. Required. |
disabled | boolean | false | Disables drag interaction and reduces opacity. |
children | ReactNode | — | Item content. |
className | string | — | Additional CSS classes. |
Draggable.Handle
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Custom content. Defaults to grip icon. |
className | string | — | Additional CSS classes. |
Draggable.Preview
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Not used. Content is cloned from the active item. |
className | string | — | Additional CSS classes applied to the preview. |
Draggable.Placeholder
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional CSS classes on the marker. |
Draggable.DropIndicator
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Additional CSS classes on the indicator. |