Skip to content

Sortable List

Reorder a list by dragging or using the keyboard, with each move announced.

import { SortableList, SortableListHandle, SortableListItem } from '@youj/react'

Installation & usage

npm i @youj/react

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Use a sortable list when the order itself is the data: priorities, a playlist, the steps of a pipeline, the columns of a table. The list owns dragging, focus and announcements; you render what's inside each row. When items also move between groups, use a Kanban board. When people sort by a field rather than by hand, use a data table.

Each item has a value, and the list's value is those values in order. Control it with value and onValueChange, and render the items in that order.

import { useState } from 'react'
import { SortableList, SortableListHandle, SortableListItem } from '@youj/react'

export function Priorities({ items }: { items: { id: string; title: string }[] }) {
  const [order, setOrder] = useState(items.map((i) => i.id))
  const byId = new Map(items.map((i) => [i.id, i]))
  return (
    <SortableList aria-label="Priorities" value={order} onValueChange={setOrder}>
      {order.map((id) => (
        <SortableListItem key={id} value={id} textValue={byId.get(id)!.title}>
          <SortableListHandle />
          {byId.get(id)!.title}
        </SortableListItem>
      ))}
    </SortableList>
  )
}

onValueChange fires once, when a row is dropped somewhere new. It doesn't fire on each step of a drag or when a move is canceled, so it's a safe place to save. Without value, the list keeps its own order, starting from defaultValue or the children's order.

Give each item a textValue. Announcements use it ("Picked up Audit log export, position 3 of 5."), and so does the handle's name ("Reorder Audit log export"). Without one, announcements read the row's text.

Reference

Propsvalue, defaultValue, onValueChange… (10)

Props

SortableList

SortableList props
PropType
valuestring[]

The items' values in order (controlled).

–
defaultValuestring[]

The starting order when uncontrolled. Default: the children's order.

–
onValueChange(value: string[]) => void

Called with the new order when a row is dropped somewhere new.

–
disabledboolean

Turn reordering off; the rows stay as a plain list.

default false

false
aria-labelstring

Names the list for assistive tech.

–
childrenReactNode–
Other props pass through: extends ComponentProps<'ul'>.

SortableListItem

SortableListItem props
PropType
valueRequiredstring

This item's id in the list's value.

–
textValuestring

What announcements call this item. Default: its text.

–
asHandleboolean

The whole row is the handle: it takes focus and starts a drag from anywhere on it.

default false

false
Other props pass through: extends ComponentProps<'li'>.

SortableListHandle

SortableListHandle props
PropType
aria-labelstring

Names the handle. Default: "Reorder" plus the item's textValue.

–
Other props pass through: extends ComponentProps<'button'>.
MotionNeighbors make room, drop, grip… · reduced motion

Motion

Motion tokens
MomentTokens
Neighbors make roomrows glide aside as the dragged row passes them, and on each keyboard step
spring.smooth

0.36s, bounce 0

0.36s, bounce 0
Dropthe row settles into its slot from wherever the pointer let go
spring.smooth

0.36s, bounce 0

0.36s, bounce 0
Gripits dots fuse into bars while the row is held
spring.snappy

0.2s, bounce 0

0.2s, bounce 0
Liftthe row scales to 1.015 and gains a shadow and a ring-colored border
duration.fastease.standard

140ms · cubic-bezier(0.2, 0, 0, 1)

140mscubic-bezier(0.2, 0, 0, 1)

Reduced motion

Rows jump to their new places and the lifted row doesn't scale. A pointer drag still follows the pointer.

The dragged row stays in the list (a drag on the row itself with layout on its neighbors, the same motion layer the Kanban board uses, not motion's Reorder), so screen readers and the DOM order always agree with what's on screen.

Reduced motion

Rows move to their new places at once, keyboard moves included, and the lifted row doesn't scale. Dragging with a pointer still follows the pointer, because that movement is the person's own.

AccessibilityKeyboard, ARIA, focus

Accessibility

  • The list is a list of listitems. Name it with aria-label or aria-labelledby.
  • The handles are a single tab stop. ↑ and ↓ move focus between them, and Home and End jump to the first and last.
  • Space or Enter picks the focused row up. While it's lifted, ↑ and ↓ move it one place, Home and End move it to either end, Space or Enter drops it, and Escape puts it back. Moving focus away also puts it back.
  • A handle is a <button> with aria-pressed while its row is lifted, and describes these keys through aria-describedby.
  • A polite live region announces each pick-up, move, drop and cancel with the row's position.
  • Escape also cancels a pointer drag: the row settles back into its old slot.