ComponentsEditors & drag-and-drop

Sortable

The kit's one reordering primitive: lists, grids, tables and multi-list boards with pointer, touch and keyboard dragging, spoken announcements and a Move menu on every item so nothing requires dragging. Also ships usePointerDrag and useTimeScale, the drag and time-axis hooks behind the scheduler and Gantt.

dnd-kit (internal; no engine types in the public API) sortable/sortable.tsx, sortable/sortable-list.tsx, sortable/use-pointer-drag.ts, scheduler/core/time-scale.tstheme · material · accent · density · font
Drag, or use the arrows menu
  • Home/
  • Services/services
  • Case studies/work
  • Pricing/pricingDraft
  • About us/about
  • Contact/contact

Installation

$ pnpm add @vamos-ui/react

The whole kit as one package — get updates by bumping the version. Add the styles to globals.css: @import "@vamos-ui/react/styles.css";

Usage

import { SortableList } from "@vamos-ui/react/sortable";

<SortableList
  aria-label="Main menu"
  items={pages}
  getLabel={(p) => p.title}
  onReorder={setPages}
  renderItem={(p) => p.title}
/>

// compose your own markup
<Sortable items={lines} onReorder={setLines}>
  {lines.map((l) => (
    <SortableItem key={l.id} id={l.id} as="tr"><td><SortableHandle /></td>…<td><SortableMoveMenu /></td></SortableItem>
  ))}
  <SortableOverlay>{(id) => <Row id={id} />}</SortableOverlay>
</Sortable>

Keyboard: focus a handle, Space or Enter lifts, arrow keys move, Space or Enter drops, Escape cancels. Instructions are attached to every handle via aria-describedby.

Announcements come from the sortable dictionary namespace, e.g. "Picked up About us. Position 2 of 6." (tr: "Hakkımızda alındı. Konum 2 / 6.").

Repeated items never blur. The overlay is an opaque popover without backdrop-filter, so it renders the same in glass and solid. Reduced motion turns off the item-shift and drop animations.

Touch: long press (220 ms) lifts, so the list still scrolls. The Move menu is the dependable path on touch screens.

Tree reordering: projectTreeMove, flattenTree and moveTreeNode (core) are ready; the SortableTree component is documented under Sortable list / tree (/docs/components/sortable-list).

Examples

Grid

A CMS article gallery. The whole tile is the drag activator; arrow keys move in two dimensions and the Move menu says Earlier / Later.

  • Cover
    Harbour at dawnharbour-dawn.jpg · 2400 × 1600
  • Spice market, Eminönüspice-market.jpg · 1800 × 1200
  • Ferry deck at noonferry-deck.jpg · 2048 × 1365
  • Galata rooftopsgalata-rooftops.jpg · 2400 × 1600
  • Tea garden in Modatea-garden.jpg · 1600 × 1067
  • Bridge lightsnight-bridge.jpg · 2400 × 1350

The first image becomes the article cover.

Between lists

HR export columns: drag between Included and Available, or use Move to list. onMoveBetween fires once on drop with the final index.

Included in export4

  • Full nameText
  • Work emailEmail
  • DepartmentSelect
  • Start dateDate

Available columns3

  • ManagerRelation
  • Gross salaryMoney
  • IBANBank

Move menu

CRM pipeline stages. Every row has a Move menu (WCAG 2.5.7): up, down, to top, to bottom; impossible moves are disabled and the result is announced.

Pipeline stages

Every row has a Move menu, so reordering never requires dragging.

  • New lead10% win rate
  • Qualified25% win rate
  • Demo booked40% win rate
  • Proposal sent60% win rate
  • Negotiation80% win rate

Horizontal

Saved views in a toolbar. Left and right arrows move the lifted item and follow the visual order, so they mirror in RTL.

  • My open deals18
  • Closing this month7
  • No activity 14 days5
  • Enterprise11

Disabled items and empty

Required onboarding steps keep their position (no handle, no menu). An empty list renders the empty slot.

Onboarding steps

  • Sign employment contractHR · Required, fixed position
  • Collect laptop and badgeIT
  • Meet your onboarding buddyTeam lead
  • Complete security trainingIT · Required, fixed position

Custom steps

No custom steps

Add steps that only apply to this department.

Composable parts

Invoice lines in a table: Sortable + SortableItem as tr + SortableHandle + SortableMoveMenu, and a custom overlay. Amounts use exact decimal strings.

Invoice lines
DescriptionQtyPriceAmountActions
Discovery workshop (2 days)2 day€1,450.00€2,900.00
UX design — booking flow36 hour€95.00€3,420.00
Front-end build80 hour€85.00€6,800.00
Hosting, first year1 year€1,200.00€1,200.00

1,000 rows, virtualized

keepMountedRange (a rangeExtractor for @tanstack/react-virtual, or any windowing) keeps the dragged row mounted while it scrolls out of view; the overlay carries the clone.

  • 1Linen side tableHM-10000
  • 2Oak side tableHM-10007
  • 3Ceramic side tableHM-10014
  • 4Wool side tableHM-10021
  • 5Copper side tableHM-10028
  • 6Walnut side tableHM-10035
  • 7Cotton side tableHM-10042
  • 8Brass side tableHM-10049
  • 9Linen throwHM-10056
  • 10Oak throwHM-10063
  • 11Ceramic throwHM-10070
  • 12Wool throwHM-10077
  • 13Copper throwHM-10084
  • 14Walnut throwHM-10091
  • 15Cotton throwHM-10098
  • 16Brass throwHM-10105
  • 17Linen pendant lampHM-10112
  • 18Oak pendant lampHM-10119
  • 19Ceramic pendant lampHM-10126
  • 20Wool pendant lampHM-10133
  • 21Copper pendant lampHM-10140

usePointerDrag + useTimeScale

The non-list drag hook used by the scheduler and Gantt: pointer capture, axis lock, 15-minute snapping on a time scale in Europe/Istanbul, Esc to cancel, and an arrow-key alternative.

9:00 AM
10:00 AM
11:00 AM
12:00 PM
1:00 PM
2:00 PM
3:00 PM
Design review11:00 AM – 12:30 PM

Drag the event (snaps to 15 minutes, Esc cancels) or focus it and use ← →.

API

SortableList / SortableGrid

PropTypeDefault
items
Items in their current order.
T[]—
onReorder
Called on drop or Move-menu move with the reordered array.
(next: T[], e: { id, from, to }) => void—
renderItem
Row / tile content. Also renders the dragged clone (overlay: true).
(item, { index, isDragging, overlay, disabled }) => ReactNode—
getId
Stable id.
(item) => stringitem.id
getLabel
Spoken in announcements and control names ("Reorder About us").
(item) => stringlabel / title / name
disabled
Fixed items: no handle, no Move menu, not draggable.
boolean | (item) => boolean—
moveMenu
Move menu on every item: the non-drag alternative.
booleantrue
renderActions
Trailing actions beside the Move menu.
(item) => ReactNode—
empty
Rendered instead of the list when items is empty.
ReactNode—
orientation
SortableList only. Grid always moves in 2-D.
"vertical" | "horizontal""vertical"
handle
Drag only from the grip handle, or from the whole item.
booleanList true · Grid false
variant
SortableList only. Separate rows, one box with dividers, or no chrome.
"card" | "inset" | "plain""card"
size
SortableList only. Row height follows --control-h.
"sm" | "md""md"
minItemWidth
SortableGrid only. Columns auto-fill at this minimum width.
string"10rem"
labels
Built-in strings (announcements, menu). Dictionary namespace: sortable.
Partial<SortableLabels>—
aria-label / aria-labelledby
Name of the list. Required for a meaningful list.
string—

Sortable

PropTypeDefault
items / getId / getLabel / onReorder / disabled / labels
Same as SortableList.
as above—
orientation
Keyboard axes, sorting strategy and the axis lock.
"vertical" | "horizontal" | "grid""vertical"
handle
true: put a SortableHandle in each item. false: the SortableItem itself is the activator.
booleantrue
containers
Multi-list mode (boards, Included / Available). items must hold every item.
{ id, label?, items: string[] }[]—
onMoveBetween
Multi-list mode: called once per drop or menu move, also within one list.
(e: { id, from, to, index, fromIndex }) => void—
renderOverlay
Default content of <SortableOverlay/>.
(item) => ReactNode—
onDragStart / onDragEnd
Lifecycle. <html data-dragging> is set while dragging so glass blur can switch off.
(id) => void / (id, cancelled) => void—

SortableItem

PropTypeDefault
id
Item id.
string—
as
Element. Keep handle mode inside lists and tables so row semantics stay intact.
"div" | "li" | "tr" | "section" | "article""div"
disabled
Overrides the root disabled rule.
boolean—

SortableHandle

PropTypeDefault
size
24 or 28 px. Named "Reorder {item}"; hidden for disabled items.
"sm" | "md""md"
children
Custom icon.
ReactNodegrip icon

SortableOverlay

PropTypeDefault
children
The dragged clone. Portaled, opaque popover, never blurred. Add vui-sortable-clone to your root for the lifted style.
ReactNode | (id) => ReactNode—

SortableMoveMenu

PropTypeDefault
id
Item to move.
stringsurrounding item
size
Trigger size.
"xs" | "sm" | "md""sm"
trigger
Custom trigger (asChild).
ReactElement—
align
Menu alignment.
"start" | "end""end"

SortableContainer

PropTypeDefault
id
Container id from containers.
string—
as
Element. Also the drop target when empty (data-empty, data-over).
"div" | "ul" | "ol" | "section""div"
children
Renders the live (preview) order while dragging.
(ids: string[], { isOver, isEmpty }) => ReactNode—

useSortableItem(id)

PropTypeDefault
returns
For kit consumers with custom markup: spread handleProps on the activator, ref + style on the root.
{ ref, handleProps, style, isDragging, isPlaceholder, disabled, index, label }—

usePointerDrag(options)

PropTypeDefault
axis
Axis lock.
"x" | "y" | "both""both"
snap
Map raw movement to a grid; snapToGrid({ y: 12 }) is provided.
(delta, state) => delta—
threshold
Pixels before a press becomes a drag; below it onTap fires.
number3 (touch 6)
scrollContainer / autoScrollEdge / autoScrollSpeed
Auto-scroll near the container edges; scroll is added to delta.
RefObject | () => el / number / number— / 32 / 14
onStart / onMove / onEnd / onCancel / onTap
state: { origin, delta, rawDelta, pointerType, altKey, shiftKey, target }. Esc and pointercancel call onCancel.
(state) => void—
returns
Spread bind on the element (pointer down + touch-action).
{ bind, isDragging, cancel }—

useTimeScale(options) / createTimeScale

PropTypeDefault
start / end
Range. Date-only values are local midnight in timeZone.
Date | number | ISO string | YYYY-MM-DD—
scale
Zoom preset (px per day + tick units).
"hour" | "day" | "week" | "month" | "quarter""day"
pxPerDay / timeZone / locale / workCalendar
Density, zone for ticks, week start, working-day marks on day ticks.
number / string / string / WorkCalendar— / "UTC" / "en-US" / —
returns
toPx, toTime, durationToPx, pxToDuration, snap(t, unit | minutes), ticks(unit), visibleRange(scrollLeft, width), dateOf, width.
TimeScale—