Items you put in order by dragging: a menu, priorities, a table's columns, a product's photos.
$ pnpm dlx shadcn@latest add @dashral/sortableInstallation
$ pnpm dlx shadcn@latest add @dashral/sortableAdds the component, the components it builds on, its npm packages and the theme tokens it uses. Or by its full URL: https://dashral.com/r/sortable.json
When to use
- Putting things in order by dragging: menu items, a playlist, priorities; keyboard works too.
When not to use
- Sorting by a column: Data Table.
- Moving cards between columns: a board.
Usage
Save the new order in onReorder.
"use client"
import { Sortable } from "@/components/dashral/sortable"
const pages = [
{ id: "home", title: "Home" },
{ id: "pricing", title: "Pricing" },
{ id: "blog", title: "Blog" },
]
export default function MenuOrder() {
return (
<Sortable
variant="card"
aria-label="Menu order"
items={pages}
onReorder={(items) => console.log("save order", items.map((item) => item.id))}
/>
)
}Using the npm package? Import from @dashral/ui/components/sortable instead, here and in the examples; the rest is the same.
Examples
Priorities
numbered and dragBy="item": the whole row moves and the numbers count along.
Table columns
size="sm", a Checkbox per column, and the first column disabled: it stays first, and nothing can be dropped above it.
API reference
Sortable
Also takes every prop of <ul>(opens in a new tab).
| Prop | Type | Default |
|---|---|---|
items | { id, title, description?, media?, meta?, actions?, label?, disabled? }[]The items, in order. Each id is unique and stays the same. Required. | — |
onReorder | (items, { id, from, to }) => voidThe new order after each drop, and the move that made it. Without it the list keeps its order itself. | — |
layout | "list" | "grid"Rows that move up and down, or tiles that move anywhere. | "list" |
dragBy | "handle" | "item"Pick up by a grip at the start, or by the whole item. Its own buttons and fields never start a drag. | "handle" in a list, "item" in a grid |
numbered | booleanIts position before each item, counting along while you drag. | false |
variant | "default" | "card"A list's rows on the page, or inside a bordered card. | "default" |
divided | booleanLines between a list's rows. | true |
size | "sm" | "default"sm for popovers, sidebars and dense panels. | "default" |
disabled | booleanNothing can move. An item's own disabled keeps that one in place: it can't be picked up, and nothing is dropped past it. | false |
empty | ReactNodeShown in place of the items when there are none. | — |
classNames | { item, handle, media, title, description, meta, actions }Classes for each part. | — |
className | stringExtra classes, merged with cn() so yours win. | — |
Keyboard
| Keys | Action |
|---|---|
| Tab | To each grip (or item), then its own controls |
| SpaceorEnter | Pick up the focused item, or drop it |
| Arrow keys | Move the item you hold |
| Escape | Put it back where it was |
Accessibility
- A <ul role="list">. Each grip (or item, with dragBy="item") is a button announced as a sortable item, described by how to move it from the keyboard.
- Picking up, each move and the drop are announced with the item's name and its position ("position 2 of 5"), in the kit's words; give label when title isn't text.
- A touch on an item waits for a press and hold, so swiping still scrolls the page; a grip starts at once.
Motion
- The others slide out of the way (--duration-standard, --ease-standard), and the item you hold rises with a shadow. With reduced motion they move without sliding.