VayuUI

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 draggable

Usage

List — Drag or keyboard (Space → Arrow ↑↓ → Space)
Press Space to grab, arrow keys to move, Space to drop, Escape to cancel.

Inbox

12 unread

Press Space to grab, arrow keys to move, Space to drop, Escape to cancel.

Photos

3,429 items

Press Space to grab, arrow keys to move, Space to drop, Escape to cancel.

Documents

156 files

Press Space to grab, arrow keys to move, Space to drop, Escape to cancel.

Music

2,847 tracks

Press Space to grab, arrow keys to move, Space to drop, Escape to cancel.

Videos

89 clips

Press Space to grab, arrow keys to move, Space to drop, Escape to cancel.

Archives

24 files

Grid — Drag or keyboard (Space → Arrow ←→↑↓ → Space)
Press Space to grab, arrow keys to move, Space to drop, Escape to cancel.
12

Inbox

Press Space to grab, arrow keys to move, Space to drop, Escape to cancel.
3,429

Photos

Press Space to grab, arrow keys to move, Space to drop, Escape to cancel.
156

Docs

Press Space to grab, arrow keys to move, Space to drop, Escape to cancel.
2,847

Music

Press Space to grab, arrow keys to move, Space to drop, Escape to cancel.
89

Videos

Press Space to grab, arrow keys to move, Space to drop, Escape to cancel.
24

Archives

Press Space to grab, arrow keys to move, Space to drop, Escape to cancel.
7

Starred

Press Space to grab, arrow keys to move, Space to drop, Escape to cancel.
31

Favorites

Press Space to grab, arrow keys to move, Space to drop, Escape to cancel.
5

Quick

Cross-list — Drag items between columns

To Do (3)

Press Space to grab, arrow keys to move, Space to drop, Escape to cancel.
Write unit tests
Press Space to grab, arrow keys to move, Space to drop, Escape to cancel.
Review PR #42
Press Space to grab, arrow keys to move, Space to drop, Escape to cancel.
Update changelog

Done (2)

Press Space to grab, arrow keys to move, Space to drop, Escape to cancel.
Setup CI pipeline
Press Space to grab, arrow keys to move, Space to drop, Escape to cancel.
Design token audit
<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 list and grid layouts 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.Container uses role="list" (list layout) or role="grid" (grid layout).
    • Draggable.Item uses role="listitem" (list) or role="gridcell" (grid).
    • aria-grabbed on Draggable.Item reflects whether the item is currently being dragged.
    • aria-roledescription="sortable item" on Draggable.Item describes the interactive role.
    • aria-disabled on Draggable.Item when the disabled prop is set.
    • aria-describedby links items to hidden instructions explaining keyboard controls.
    • aria-label on Draggable.Container and Draggable.Handle provides accessible labels.
  • Focus Behavior:
    • Roving tabindex pattern — only the focused item has tabIndex={0}, all others have tabIndex={-1}.
    • focus-visible:ring-2 focus-visible:ring-focus ring provides visible focus indication.
    • During keyboard drag, the active item receives a brand-colored ring and elevated shadow.

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 assembly

Props

Draggable (Root)

PropTypeDefaultDescription
itemsstring[]Controlled array of item IDs for single-list mode.
defaultItemsstring[][]Initial item IDs for uncontrolled single-list mode.
onReorder(items: string[]) => voidCallback after single-list reorder.
containersContainersMapControlled multi-container state for cross-list dragging.
defaultContainersContainersMap{}Initial multi-container state for uncontrolled mode.
onContainersChange(containers: ContainersMap) => voidCallback after multi-container reorder.
childrenReactNodeDraggable.Container with items and optional overlays.
classNamestringAdditional CSS classes.

Draggable.Container

PropTypeDefaultDescription
layout"list" | "grid""list"Layout mode for items.
columnsnumber3Number of grid columns (grid mode only).
containerIdstringUnique ID for multi-container mode.
aria-labelstring"Sortable list"Accessible label for the container.
childrenReactNodeDraggable.Item elements.
classNamestringAdditional CSS classes.

Draggable.Item

PropTypeDefaultDescription
valuestringUnique identifier matching an entry in items. Required.
disabledbooleanfalseDisables drag interaction and reduces opacity.
childrenReactNodeItem content.
classNamestringAdditional CSS classes.

Draggable.Handle

PropTypeDefaultDescription
childrenReactNodeCustom content. Defaults to grip icon.
classNamestringAdditional CSS classes.

Draggable.Preview

PropTypeDefaultDescription
childrenReactNodeNot used. Content is cloned from the active item.
classNamestringAdditional CSS classes applied to the preview.

Draggable.Placeholder

PropTypeDefaultDescription
classNamestringAdditional CSS classes on the marker.

Draggable.DropIndicator

PropTypeDefaultDescription
classNamestringAdditional CSS classes on the indicator.

On this page