ComponentsEditors & drag-and-drop

Sortable list / tree

Ready reorderable lists, grids and nested trees: menu builders, category order, onboarding steps, dashboard tiles, territory trees. Every item has a drag handle and a Move menu (up, down, top, bottom, and for trees indent, outdent and move into…), so nothing requires dragging. Trees pick the depth from the horizontal drag offset and enforce a max depth and your own drop rules.

Sortable (dnd-kit, internal) + Tree view semantics sortable/sortable-tree.tsx, sortable/sortable-list.tsx, sortable/core.tstheme · material · accent · density · font
Up to 3 levels · drag sideways to nest
Home
Services
Branding
Websites
Online shops
Booking sites
Ad management
Case studies
Journal
System status External
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, SortableGrid, SortableTree, moveTreeNode } from "@vamos-ui/react/sortable";

<SortableList aria-label="Steps" items={steps} onReorder={setSteps} renderItem={(s) => s.title} />

<SortableTree
  aria-label="Main menu"
  nodes={menu}
  maxDepth={3}
  canDrop={(c) => (c.parentId === "status" ? "Links can't have sub-items" : true)}
  onMove={(e) => setMenu((m) => moveTreeNode(m, e.id, e.parentId, e.index))}
/>

Trees use the Tree view pattern: role="tree", one tab stop, ↑ / ↓ move between rows, → opens or enters, ← closes or goes to the parent, Home / End, * opens siblings, type-ahead by first letter.

Moving from the keyboard: Space or Enter lifts the focused row, ↑ / ↓ move it, ← / → change its level, Space drops, Esc cancels. Without lifting: Alt + ↑ / ↓ moves among siblings, Alt + → indents, Alt + ← outdents (mirrored in RTL), Shift+F10 opens the Move menu.

Announcements say where the row will land ("About us will move into Services, position 3.") and the result after a drop or menu move. Focus stays on the moved row.

The dragged clone is an opaque popover without blur, so glass and solid render the same. Reduced motion turns off the shift and drop animations.

Examples

Vertical list

HR onboarding steps in one inset box. Required steps are fixed: no handle, no Move menu, and they keep their slot.

Onboarding checklist — Engineering

  • 1Sign employment contractPeople team · Before day 1Fixed
  • 2Laptop and accounts readyIT · Day 1
  • 3Meet your onboarding buddyManager · Day 1
  • 4Security awareness trainingIT · Week 1
  • 5Set 30/60/90-day goalsManager · Week 2
  • 6Probation check-inManager · Month 3Fixed

Grid

CRM dashboard tiles. The whole tile is the drag activator and arrow keys move it in two dimensions.

  • Monthly recurring revenue$128K▲ 6.2% vs last month
  • Open pipeline$1.2M▲ 11% vs last month
  • Win rate28.4%▼ 1.3% vs last month
  • Deals closing this month23▲ 21% vs last month
  • Avg. sales cycle (days)41▼ 8% vs last month
  • Customer NPS52▲ 4% vs last month

Compact tree

Shop categories at size sm with a 20px indent and two levels. Only Brewing equipment starts expanded; a collapsed parent still accepts children.

Brewing equipment61
Grinders9
Kettles7
Pour-over & filters31
Espresso machines14
Tea27
Gift sets8

Rules and async saving

PRM territories: canDrop keeps regions at the top and territories under a region. onMove returns a promise, so the move shows at once and rolls back with a Retry toast if the server rejects it.

EMEA
DACH Alpine Systems GmbH
Nordics Fjordline AS
Türkiye Boğaziçi Bilişim
Americas
US West Pacific Crest Partners
Latin America Andes Digital
APAC
Australia & NZ Southern Cross IT

API

SortableTree

PropTypeDefault
nodes
The tree. Rows show label / title / name unless renderItem is set.
N[] (N extends { id, children? })—
onMove
Once per drop or menu move. Apply it with moveTreeNode(nodes, id, parentId, index). A promise keeps the move optimistic; a rejection rolls it back.
(e: { id, parentId, index, fromParentId, fromIndex }) => void | Promise<void>—
getLabel
Row text, type-ahead, announcements and the Move menu name.
(node) => stringlabel / title / name
maxDepth
Levels allowed, counting the moved subtree ("Max depth 3").
number—
canDrop
A string blocks the drop: red indicator and reason on the dragged clone, disabled menu items with the reason.
(c: { id, parentId, index, depth, fromParentId, fromIndex }) => true | string—
disabled
Fixed rows: not draggable, no Move menu. They still accept children.
(node) => boolean—
expanded / defaultExpanded / onExpandedChange
Open rows. Moving into a parent opens it.
string[]every parent
indent
px per level; also the horizontal drag distance that changes one level.
number24
guides
Vertical indent guides.
booleantrue
size
Row height 32 / 36 px.
"sm" | "md""md"
renderItem
Row content (icon, text, badges).
(node, { depth, isDragging, overlay, expanded, childCount }) => ReactNode—
renderActions
Trailing buttons before the Move menu.
(node) => ReactNode—
moveMenu
Move menu on every row (opened with Shift+F10 from the keyboard).
booleantrue
empty
Shown when nodes is empty.
ReactNode—
selected / onSelect
Selectable rows: click or Enter selects (aria-selected), Space still picks a row up for a keyboard move. The toggle, row actions and the Move menu keep their own click.
string | null / (node) => void—
labels
Built-in strings. Dictionary namespace: sortableTree.
Partial<SortableTreeLabels>—
aria-label / aria-labelledby
Name of the tree.
string—

SortableList / SortableGrid

PropTypeDefault
items / onReorder / renderItem
Items in order, the reordered array on every move, row content.
T[] / (next, e) => void / (item, state) => ReactNode—
getId / getLabel / disabled
Stable id, spoken name, fixed items.
(item) => string / (item) => string / boolean | (item) => boolean—
variant / size / orientation
SortableList only.
"card" | "inset" | "plain" / "sm" | "md" / "vertical" | "horizontal""card" / "md" / "vertical"
minItemWidth
SortableGrid only: auto-fill column width.
string"10rem"
moveMenu / renderActions / empty / labels
See the Sortable page for the full list.
boolean / (item) => ReactNode / ReactNode / Partial<SortableLabels>—

Core (React-free)

PropTypeDefault
moveTreeNode(nodes, id, parentId, index)
Immutable move of a node (and its subtree).
N[]—
projectTreeMove(flat, activeId, overId, offsetX, indent, { maxDepth, dir })
Where a dragged row would land; depth from the horizontal offset (sign flipped in RTL).
{ parentId, depth, index, invalid? } | null—
flattenTree / descendantIds / arrayMove
Helpers shared with Data Table, Tree view and Line items.
——