useReorder

useReorder({ onMove }) lets a person reorder a list by dragging: with the browser's drag and drop for a mouse, and with pointer events for a finger or a pen. Spread getItemProps(index) on each item and getHandleProps(index) on the element inside it that a drag starts from, such as a grip. onMove(from, to) gets the dragged item's index and the index it ends at.

On this page
import { useReorder } from '@tounsoo/todae';
import { GripVertical } from 'lucide-react';

const reorder = useReorder({ onMove: move });

<ol>
  {items.map((item, index) => (
    <li key={item.id} {...reorder.getItemProps(index)}>
      <span {...reorder.getHandleProps(index)} aria-hidden style={{ cursor: 'grab', touchAction: 'none' }}>
        <GripVertical />
      </span>
      {item.name}
    </li>
  ))}
</ol>

Drag a row by its grip. The Reorder recipe pairs the hook with a menu, for the keyboard and for anyone who can't drag.

Pinned channels

  1. Design

  2. Engineering

  3. Marketing

  4. Sales

  5. Support

A press on a handle makes its item draggable, so an item drags only by its handle and text in the items stays selectable. While an item is dragged, dragging is its index and drop says which item it would land beside and on which side, before or after, or is null where a drop would not move it. The items carry the same state as data-dragging and data-drop, for styles. A drop calls onMove only when the order changes, and a drop outside the items calls nothing. from is where the dragged item is when it is dropped, so a list that changes during a drag still moves that item, and a drag whose item is removed ends. Drags that didn't start from this list's handles, such as files, selected text or another list's items, are ignored, so they can't be dropped there. Both getters take your own props as a second argument and run your handlers before their own; a handler of yours that calls preventDefault() skips the hook's, as in every Todae part.

With orientation="horizontal", before is the side the text starts from, so it is the right side in a right-to-left page.

Escape cancels a drag and leaves the order as it was. During a mouse drag the browser takes Escape itself and ends the drag with no drop; during a touch drag the hook takes it before a Dialog around the list, which stays open until the next Escape.

Touch

Browsers on touch screens differ: some start the browser's drag after a long press and some never do. So a finger or a pen on a handle drags with pointer events instead, the same way everywhere: the item drags once the finger or pen moves, drop follows the item under it, and lifting it drops the item. A tap drags nothing, and a pen drag selects no text. The handle needs touch-action: none: without it the browser takes a finger or a pen to scroll the page, which cancels the drag. The page doesn't scroll during a touch drag, so in a list taller than the screen the menu is the way to move an item far.

Accessible composition

Todae handles: Dragging an item only by its handle, with a mouse, a finger or a pen. Working out the item and side a drop lands on, and the index the dragged item ends at. Ignoring drags from elsewhere. Cancelling a drag on Escape.

You supply: Another way to move an item that needs no dragging and works from the keyboard, such as the Reorder recipe's menu: the browser's drag and drop has no keyboard path. A handle hidden from screen readers with aria-hidden, since that other way is the one they use. Keys from each item's id. The announcement of a move, and how the drop line and the dragged item look.

Watch for: Drag as the only way to reorder (WCAG 2.2 SC 2.1.1 and SC 2.5.7). A handle that is a button or a tab stop: it does nothing from the keyboard. A handle smaller than 24 by 24 CSS pixels, such as a bare icon (SC 2.5.8). A handle without touch-action: none, which a press on a touch screen scrolls instead of dragging. Touch dragging is tested with Chromium's touch input, was checked with pointer events in Firefox and WebKit, and hasn't been checked on a phone.

When to use

When: Letting a person drag the items of a list they order themselves, beside a way to move them without dragging.

When not: Dropping files → useDropzone. Moving items between lists → your product's own drag and drop.

Props

useReorder props
NameTypeDescription
onMove Required(from: number, to: number) => void

Called on a drop that changes the order, with the dragged item's index and the index it ends at.

orientation'vertical' | 'horizontal'

The axis the items run along. Default 'vertical'. In a horizontal list, 'before' follows the text direction.

Default: 'vertical'

disabledboolean

Turn dragging off. While `disabled` is true, a drag under way can't be dropped.

Default: false