Lift list
FreeA list you reorder by picking a row up. The row you hold comes off the page, rises toward you and leans a few degrees the way you move, and a dashed slot shows where it will land. Space picks up, the arrows move, Space drops, Escape puts it back.
pnpm dlx shadcn@latest add @caliper/lift-list- Write release notesMira
- Review pricing copyJo
- Fix 320px overflow on the indexAasis
- Record motion clipsSam
- Publish changelogMira
Drag the grip, or focus it and press Space, then use the arrow keys.
When to use #
Use it for
- Priorities, checklists and queues where order matters and the list is short.
- Anywhere a drag handle needs a clear picked-up state.
Skip it when
- Long lists; use the sortable list with scrolling.
- Tables, which have their own row behaviour.
Decisions #
Why it is built this way, so you know what to keep when you change your copy.
Behaviour
- The slot stays visible
- Lift never hides the landing place: the dashed slot moves with the order while the lifted row follows your pointer.
- Only the grip drags
- On touch, the rest of the row still scrolls the page. The grip is 44px tall and has touch-action none.
Styling
- Lean is limited and tied to speed
- At most 7 degrees, from the drag velocity, and none under reduced motion.
Installation #
pnpm dlx shadcn@latest add @caliper/lift-list
Adds its 5 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above.
import { useState } from "react" import { LiftList, type LiftItem } from "@/components/ui/lift-list" const initialTasks: LiftItem[] = [ { id: "notes", label: "Write release notes", name: "Write release notes", meta: "Mira" }, { id: "copy", label: "Review pricing copy", name: "Review pricing copy", meta: "Jo" }, { id: "overflow", label: "Fix 320px overflow on the index", name: "Fix 320px overflow on the index", meta: "Aasis" }, { id: "clips", label: "Record motion clips", name: "Record motion clips", meta: "Sam" }, { id: "changelog", label: "Publish changelog", name: "Publish changelog", meta: "Mira" }, ]; export function LiftListDemo() { const [items, setItems] = useState(initialTasks); return ( <div className="w-full max-w-lg"> <LiftList label="Launch checklist" items={items} onReorder={(ids) => setItems((cur) => ids.map((id) => cur.find((item) => item.id === id)!))} /> </div> ); }
API reference #
LiftList
component
Accessibility #
It is built on native elements and keeps aria-label, aria-pressed, aria-hidden and aria-live in step with state. Changes that matter are announced through a live region.
Motion #
It checks prefers-reduced-motion itself and settles immediately instead of travelling. Every state is still reachable.