Mischief

89 / Controls

Sortable List

A list reordered by dragging a handle, or from the keyboard without one, where every move is announced.

  1. Read the brief

    Twice, slowly

  2. Sketch three routes

    On paper

  3. Build the likely one

    Thin slice first

  4. Show someone

    Before it is finished

Drag a handle, or focus one and press space to lift it.

Installation

Copy the source into your project, or keep it behind a package.

npx shadcn@latest add Tinkerers-Labs/mischief-ui/sortable-list
import { SortableList } from "mischief-ui/sortable-list"
Also installs
  • lucide-react

Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.

registry/default/sortable-list/sortable-list.tsx
"use client" import * as React from "react"import { GripVertical } from "lucide-react"import { cn } from "@/lib/utils" export type SortableListProps<TItem> = Omit<  React.HTMLAttributes<HTMLOListElement>,  "children"> & {  items: readonly TItem[]  getKey: (item: TItem) => string  onReorder: (items: TItem[]) => void  renderItem: (item: TItem, index: number) => React.ReactNode

Usage

export function Tasks() {
  const [tasks, setTasks] = React.useState(initial)

  return (
    <SortableList
      items={tasks}
      getKey={(task) => task.id}
      getLabel={(task) => task.name}
      onReorder={setTasks}
      renderItem={(task) => <p>{task.name}</p>}
    />
  )
}

Reordering without a pointer

Drag and drop is the version of this everyone builds and the version a keyboard cannot use. So the handle is a real button that can be focused, and the whole operation works from there.

  • Space or Enter lifts the item, and says so along with what to do next.
  • Up and down arrows move it while it is lifted, announcing its new position each time.
  • Space or Enter drops it.
  • Escape puts the list back the way it was before the lift.

A lifted item is marked as pressed and ringed, so its state is visible as well as announced.

Saying where things went

Every move reports the item and its new position out of the total. A list that rearranges itself in silence is unusable to anyone not watching it, and that includes anyone who dragged something and looked away.

getLabel={(task) => task.name}
Without this the announcements can only say Item 3, which is true and useless.

It owns no order of its own

The list is whatever you passed and every change comes back through onReorder, including the ones made while a drag is still in progress. There is no internal copy to fall out of step with yours, and persisting the order is a matter of saving what you were handed.

API

itemsTItem[]The list, in its current order.
getKey(item) => stringA stable key for each item.
onReorder(items) => voidThe list in its new order.
renderItem(item, index) => ReactNodeWhat each row shows.
getLabel(item) => stringNames the item on its handle and in announcements. Worth passing.
labelstringNames the list. Defaults to "Sortable list".

Accessibility

An ordered list whose handles are named buttons carrying the item's name. The full operation is available from the keyboard, with lifted state exposed as pressed, and every move and cancellation announced politely. Escape restores the order from before the lift.