Accessibility pattern · Interaction and motion

Sortable list

Dragging is one way to reorder, never the only one: every item also has Move up and Move down buttons that work with a single click or tap. On a handle, Space picks the item up, the arrow keys move it and Space drops it, and a polite live region says where it is after every step.

WCAG criteria
7
Keyboard rules
6
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Reading list

6 books

Drag a handle or use the arrow buttons. With a keyboard, Space on a handle picks a book up, the arrow keys move it, Space drops it and Escape puts it back.

  1. 1 Wings of Fire A. P. J. Abdul Kalam
  2. 2 Atomic Habits James Clear
  3. 3 The God of Small Things Arundhati Roy
  4. 4 Sapiens Yuval Noah Harari
  5. 5 The White Tiger Aravind Adiga
  6. 6 Deep Work Cal Newport

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
Tab or ShiftTabMoves through each book's handle and its Move up and Move down buttons.
Space or EnterOn a handle, picks the book up; pressed again, drops it where it now is.
Arrow Up or Arrow DownWhile a book is picked up, moves it one place up or down; focus stays on its handle.
Home or EndWhile a book is picked up, moves it to the top or the bottom of the list.
EscapeWhile a book is picked up, cancels the move and puts it back where it started.
Enter or SpaceOn a Move up or Move down button, moves that book one place; focus stays on the button.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Focus reaches a handleReorder Atomic Habits, button, then the instructions under the list's name
Space picks the book upPicked up: Atomic Habits. Position 2 of 6.
Arrow Down moves itMoved to position 3 of 6.
Arrow Up is pressed on the first placeTop of the list.
Space drops itDropped: Atomic Habits. Position 3 of 6.
Escape cancels insteadCancelled: Atomic Habits. Position 2 of 6.
A Move down button is pressedMoved to position 4 of 6.

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-sortable-list" data-ap-sortable-list>
  <div class="ap-sortable-list__top">
    <h3 class="ap-sortable-list__name" id="sortable-list-name">Reading list</h3>
    <span class="ap-sortable-list__count">6 books</span>
  </div>
  <p class="ap-sortable-list__help" id="sortable-list-help">Drag a handle or use the arrow buttons. With a keyboard, Space on a handle picks a book up, the arrow keys move it, Space drops it and Escape puts it back.</p>
  <ol class="ap-sortable-list__list" role="list" aria-labelledby="sortable-list-name" data-ap-list>
    <li class="ap-sortable-list__item" data-ap-item>
      <button type="button" class="ap-sortable-list__handle" data-ap-handle aria-labelledby="sl-grab-1 sl-book-1" aria-describedby="sortable-list-help">
        <svg class="ap-sortable-list__grip" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="9" cy="6" r="1.6"/><circle cx="15" cy="6" r="1.6"/><circle cx="9" cy="12" r="1.6"/><circle cx="15" cy="12" r="1.6"/><circle cx="9" cy="18" r="1.6"/><circle cx="15" cy="18" r="1.6"/></svg><svg class="ap-sortable-list__arrows" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m8 8 4-4 4 4M8 16l4 4 4-4M12 4v16"/></svg>
        <span class="ap-sortable-list__sr" id="sl-grab-1">Reorder</span>
      </button>
      <span class="ap-sortable-list__pos" data-ap-pos>1</span>
      <span class="ap-sortable-list__cover ap-sortable-list__cover--accent" aria-hidden="true">W</span>
      <span class="ap-sortable-list__book">
        <span class="ap-sortable-list__book-name" id="sl-book-1" data-ap-book translate="no">Wings of Fire</span>
        <span class="ap-sortable-list__by" translate="no">A. P. J. Abdul Kalam</span>
      </span>
      <span class="ap-sortable-list__moving" aria-hidden="true">Picked up</span>
      <span class="ap-sortable-list__moves">
        <button type="button" class="ap-sortable-list__move" data-ap-move="up" aria-labelledby="sl-up-1 sl-book-1" aria-disabled="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 15 6-6 6 6"/></svg><span class="ap-sortable-list__sr" id="sl-up-1">Move up</span></button>
        <button type="button" class="ap-sortable-list__move" data-ap-move="down" aria-labelledby="sl-down-1 sl-book-1" aria-disabled="false"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg><span class="ap-sortable-list__sr" id="sl-down-1">Move down</span></button>
      </span>
    </li>
    <li class="ap-sortable-list__item" data-ap-item>
      <button type="button" class="ap-sortable-list__handle" data-ap-handle aria-labelledby="sl-grab-2 sl-book-2" aria-describedby="sortable-list-help">
        <svg class="ap-sortable-list__grip" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="9" cy="6" r="1.6"/><circle cx="15" cy="6" r="1.6"/><circle cx="9" cy="12" r="1.6"/><circle cx="15" cy="12" r="1.6"/><circle cx="9" cy="18" r="1.6"/><circle cx="15" cy="18" r="1.6"/></svg><svg class="ap-sortable-list__arrows" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m8 8 4-4 4 4M8 16l4 4 4-4M12 4v16"/></svg>
        <span class="ap-sortable-list__sr" id="sl-grab-2">Reorder</span>
      </button>
      <span class="ap-sortable-list__pos" data-ap-pos>2</span>
      <span class="ap-sortable-list__cover ap-sortable-list__cover--success" aria-hidden="true">A</span>
      <span class="ap-sortable-list__book">
        <span class="ap-sortable-list__book-name" id="sl-book-2" data-ap-book translate="no">Atomic Habits</span>
        <span class="ap-sortable-list__by" translate="no">James Clear</span>
      </span>
      <span class="ap-sortable-list__moving" aria-hidden="true">Picked up</span>
      <span class="ap-sortable-list__moves">
        <button type="button" class="ap-sortable-list__move" data-ap-move="up" aria-labelledby="sl-up-2 sl-book-2" aria-disabled="false"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 15 6-6 6 6"/></svg><span class="ap-sortable-list__sr" id="sl-up-2">Move up</span></button>
        <button type="button" class="ap-sortable-list__move" data-ap-move="down" aria-labelledby="sl-down-2 sl-book-2" aria-disabled="false"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg><span class="ap-sortable-list__sr" id="sl-down-2">Move down</span></button>
      </span>
    </li>
    <li class="ap-sortable-list__item" data-ap-item>
      <button type="button" class="ap-sortable-list__handle" data-ap-handle aria-labelledby="sl-grab-3 sl-book-3" aria-describedby="sortable-list-help">
        <svg class="ap-sortable-list__grip" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="9" cy="6" r="1.6"/><circle cx="15" cy="6" r="1.6"/><circle cx="9" cy="12" r="1.6"/><circle cx="15" cy="12" r="1.6"/><circle cx="9" cy="18" r="1.6"/><circle cx="15" cy="18" r="1.6"/></svg><svg class="ap-sortable-list__arrows" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m8 8 4-4 4 4M8 16l4 4 4-4M12 4v16"/></svg>
        <span class="ap-sortable-list__sr" id="sl-grab-3">Reorder</span>
      </button>
      <span class="ap-sortable-list__pos" data-ap-pos>3</span>
      <span class="ap-sortable-list__cover ap-sortable-list__cover--warning" aria-hidden="true">T</span>
      <span class="ap-sortable-list__book">
        <span class="ap-sortable-list__book-name" id="sl-book-3" data-ap-book translate="no">The God of Small Things</span>
        <span class="ap-sortable-list__by" translate="no">Arundhati Roy</span>
      </span>
      <span class="ap-sortable-list__moving" aria-hidden="true">Picked up</span>
      <span class="ap-sortable-list__moves">
        <button type="button" class="ap-sortable-list__move" data-ap-move="up" aria-labelledby="sl-up-3 sl-book-3" aria-disabled="false"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 15 6-6 6 6"/></svg><span class="ap-sortable-list__sr" id="sl-up-3">Move up</span></button>
        <button type="button" class="ap-sortable-list__move" data-ap-move="down" aria-labelledby="sl-down-3 sl-book-3" aria-disabled="false"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg><span class="ap-sortable-list__sr" id="sl-down-3">Move down</span></button>
      </span>
    </li>
    <li class="ap-sortable-list__item" data-ap-item>
      <button type="button" class="ap-sortable-list__handle" data-ap-handle aria-labelledby="sl-grab-4 sl-book-4" aria-describedby="sortable-list-help">
        <svg class="ap-sortable-list__grip" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="9" cy="6" r="1.6"/><circle cx="15" cy="6" r="1.6"/><circle cx="9" cy="12" r="1.6"/><circle cx="15" cy="12" r="1.6"/><circle cx="9" cy="18" r="1.6"/><circle cx="15" cy="18" r="1.6"/></svg><svg class="ap-sortable-list__arrows" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m8 8 4-4 4 4M8 16l4 4 4-4M12 4v16"/></svg>
        <span class="ap-sortable-list__sr" id="sl-grab-4">Reorder</span>
      </button>
      <span class="ap-sortable-list__pos" data-ap-pos>4</span>
      <span class="ap-sortable-list__cover ap-sortable-list__cover--info" aria-hidden="true">S</span>
      <span class="ap-sortable-list__book">
        <span class="ap-sortable-list__book-name" id="sl-book-4" data-ap-book translate="no">Sapiens</span>
        <span class="ap-sortable-list__by" translate="no">Yuval Noah Harari</span>
      </span>
      <span class="ap-sortable-list__moving" aria-hidden="true">Picked up</span>
      <span class="ap-sortable-list__moves">
        <button type="button" class="ap-sortable-list__move" data-ap-move="up" aria-labelledby="sl-up-4 sl-book-4" aria-disabled="false"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 15 6-6 6 6"/></svg><span class="ap-sortable-list__sr" id="sl-up-4">Move up</span></button>
        <button type="button" class="ap-sortable-list__move" data-ap-move="down" aria-labelledby="sl-down-4 sl-book-4" aria-disabled="false"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg><span class="ap-sortable-list__sr" id="sl-down-4">Move down</span></button>
      </span>
    </li>
    <li class="ap-sortable-list__item" data-ap-item>
      <button type="button" class="ap-sortable-list__handle" data-ap-handle aria-labelledby="sl-grab-5 sl-book-5" aria-describedby="sortable-list-help">
        <svg class="ap-sortable-list__grip" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="9" cy="6" r="1.6"/><circle cx="15" cy="6" r="1.6"/><circle cx="9" cy="12" r="1.6"/><circle cx="15" cy="12" r="1.6"/><circle cx="9" cy="18" r="1.6"/><circle cx="15" cy="18" r="1.6"/></svg><svg class="ap-sortable-list__arrows" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m8 8 4-4 4 4M8 16l4 4 4-4M12 4v16"/></svg>
        <span class="ap-sortable-list__sr" id="sl-grab-5">Reorder</span>
      </button>
      <span class="ap-sortable-list__pos" data-ap-pos>5</span>
      <span class="ap-sortable-list__cover ap-sortable-list__cover--danger" aria-hidden="true">T</span>
      <span class="ap-sortable-list__book">
        <span class="ap-sortable-list__book-name" id="sl-book-5" data-ap-book translate="no">The White Tiger</span>
        <span class="ap-sortable-list__by" translate="no">Aravind Adiga</span>
      </span>
      <span class="ap-sortable-list__moving" aria-hidden="true">Picked up</span>
      <span class="ap-sortable-list__moves">
        <button type="button" class="ap-sortable-list__move" data-ap-move="up" aria-labelledby="sl-up-5 sl-book-5" aria-disabled="false"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 15 6-6 6 6"/></svg><span class="ap-sortable-list__sr" id="sl-up-5">Move up</span></button>
        <button type="button" class="ap-sortable-list__move" data-ap-move="down" aria-labelledby="sl-down-5 sl-book-5" aria-disabled="false"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg><span class="ap-sortable-list__sr" id="sl-down-5">Move down</span></button>
      </span>
    </li>
    <li class="ap-sortable-list__item" data-ap-item>
      <button type="button" class="ap-sortable-list__handle" data-ap-handle aria-labelledby="sl-grab-6 sl-book-6" aria-describedby="sortable-list-help">
        <svg class="ap-sortable-list__grip" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="9" cy="6" r="1.6"/><circle cx="15" cy="6" r="1.6"/><circle cx="9" cy="12" r="1.6"/><circle cx="15" cy="12" r="1.6"/><circle cx="9" cy="18" r="1.6"/><circle cx="15" cy="18" r="1.6"/></svg><svg class="ap-sortable-list__arrows" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m8 8 4-4 4 4M8 16l4 4 4-4M12 4v16"/></svg>
        <span class="ap-sortable-list__sr" id="sl-grab-6">Reorder</span>
      </button>
      <span class="ap-sortable-list__pos" data-ap-pos>6</span>
      <span class="ap-sortable-list__cover ap-sortable-list__cover--plain" aria-hidden="true">D</span>
      <span class="ap-sortable-list__book">
        <span class="ap-sortable-list__book-name" id="sl-book-6" data-ap-book translate="no">Deep Work</span>
        <span class="ap-sortable-list__by" translate="no">Cal Newport</span>
      </span>
      <span class="ap-sortable-list__moving" aria-hidden="true">Picked up</span>
      <span class="ap-sortable-list__moves">
        <button type="button" class="ap-sortable-list__move" data-ap-move="up" aria-labelledby="sl-up-6 sl-book-6" aria-disabled="false"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 15 6-6 6 6"/></svg><span class="ap-sortable-list__sr" id="sl-up-6">Move up</span></button>
        <button type="button" class="ap-sortable-list__move" data-ap-move="down" aria-labelledby="sl-down-6 sl-book-6" aria-disabled="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg><span class="ap-sortable-list__sr" id="sl-down-6">Move down</span></button>
      </span>
    </li>
  </ol>
  <p class="ap-sortable-list__sr" role="status" data-ap-live></p>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    The books are an ordered list, and each shows its place as a number in text that the script rewrites after every move.

  • 2.1.1 Keyboard Level A

    The keyboard pick-up mode and the Move buttons do everything dragging does, so no move needs a pointer.

  • 2.4.3 Focus Order Level A

    The script moves the other books around the one being moved, so focus never leaves its handle or button and Tab carries on from there.

  • 2.5.7 Dragging Movements Level AA

    Every drag has a single-pointer alternative: Move up and Move down buttons that move a book one place per click or tap.

  • 2.5.8 Target Size (Minimum) Level AA

    Handles are 32 by 44 pixels and the Move buttons 32 by 32, both past the 24-pixel minimum.

  • 4.1.2 Name, Role, Value Level A

    Each handle and button is named with its book, such as Reorder Atomic Habits, and Move up on the first book is marked unavailable.

  • 4.1.3 Status Messages Level AA

    Picking up, each move, dropping and cancelling are announced in a polite live region without moving focus.

Usage

When to use it

Use it

  • Lists people arrange by hand: a reading list, a playlist, priorities, the fields of a form builder.
  • Short lists where seeing the whole order at once matters.

Use something else

  • Long lists: offer a position field or a Move to top action rather than dozens of single steps.
  • An order that follows a rule, like newest first: let people pick the rule with a sort control.
  • Moving items between groups: a board with a Move to menu on each card says that better.

Common failures

How it usually goes wrong

  • Dragging as the only way

    Without another way to move items, people using a keyboard, switch access or a tremor-prone hand cannot reorder at all, and the page fails 2.5.7.

  • Moving the focused element

    Taking the focused item out of the list and putting it back drops focus to the page in some browsers. Moving its neighbours instead keeps focus where it was.

  • Reordering in silence

    If an item moves and nothing is said, a screen reader user cannot tell whether the key did anything. Each step here announces the new position.

  • Order shown only by place on screen

    A list that relies on layout alone hides its order. The numbers here are text in an ordered list, rewritten after every move.

  • No way to back out

    Without Escape, one stray arrow key is a permanent change. Escape puts the book back where it was picked up.

  • Disabled buttons that drop focus

    A Move up button given the disabled attribute when its book reaches the top loses focus mid-task. aria-disabled keeps it focusable and says it is unavailable.

Notes

Building it

  • Screen readers in browse mode send a click instead of key presses, so the arrow keys may never reach a handle; the Move buttons work in every mode, which is why they are part of the pattern, not an extra.
  • The script never moves the element that holds focus: it re-inserts the other items around it. Element.moveBefore(), where supported, is another way to keep focus during a move.
  • Moves animate with a short slide that stops when the system asks for reduced motion; the drag itself follows the pointer either way, because the person is moving it.
  • Clicking a handle without dragging also picks the book up, so a pointer user can finish the move with the arrow keys or click again to drop it.
  • Save the order as it changes, or with a single Save button, and say which; this demo keeps it in the page only.

Sources: WCAG 2.2 Understanding: Dragging Movements · WAI-ARIA Authoring Practices: Developing a Keyboard Interface

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction