caliper uidocs

Lift list

Free

A 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.

Installs components/ui/lift-list.tsxDepends on @radix-ui/react-slot, lucide-reactMotion honours reduced motion
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 #

terminal
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.

example.tsx
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
PropTypeDefault
label *string—
items *readonly LiftItem[]—
onReorder *(ids: string[]) => void—
rowHeightnumber52
gapnumber6
statusDepthStatus"ready"
messagesDepthMessages—
onRetry(() => void)—
disabledboolean—
classNamestring—

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.

KeyBehaviour
SpaceActivates the focused control.
EnterActivates the focused control.
↑Moves to the previous item or value.
↓Moves to the next item or value.
EscapeCloses or cancels, then returns focus to where it came from.

Motion #

It checks prefers-reduced-motion itself and settles immediately instead of travelling. Every state is still reachable.