Row Drag & Drop Reordering

Permalink to "Row Drag & Drop Reordering"

Drag-and-drop is how most interfaces let users reorder rows: priority lists, playlists, backlog tables, form-builder fields, kanban-like status tables. It is direct and satisfying with a mouse. For everyone else it ranges from slow to impossible. Keyboard-only users cannot drag at all. Switch users, eye-tracking users and head-pointer users can sometimes drag, but slowly and with frequent mis-drops. Screen reader users cannot perceive where the row is while it is moving. WCAG 2.2 responded with SC 2.5.7 Dragging Movements: any function performed by dragging needs a single-pointer alternative.

The failure this topic prevents is a reorderable table where the only way to change order is to drag. The fix is not complicated — move buttons, a “Move to position” command and good announcements solve most of it — but it has details that matter: focus has to follow the row, messages must keep pace with key presses, and old ARIA attributes that promised to solve the problem need to be removed because nothing ever announced them.

This topic belongs to accessible data tables & grid systems.

WCAG criteria in scope

Permalink to "WCAG criteria in scope"
Criterion Level Relevance to row reordering
1.3.2 Meaningful Sequence A DOM order reflects the visual order after every move
2.1.1 Keyboard A Reordering operable without a pointer
2.4.3 Focus Order A Focus follows the moved row
2.5.7 Dragging Movements AA Single-pointer alternative to every drag
2.5.8 Target Size (Minimum) AA Handles and arrow buttons meet 24×24 px or spacing
3.3.2 Labels or Instructions A Keys for drag mode are described
4.1.2 Name, Role, Value A Handles named; picked-up state exposed
4.1.3 Status Messages AA Positions, drops and cancellations announced

Prerequisites

Permalink to "Prerequisites"
What drag-only reordering excludes and what fixes it Comparison listing the user groups excluded by drag-only reordering and the alternative that serves each. What drag-only reordering excludes and what fixes it✗ Excluded by drag-onlyKeyboard-only usersSwitch and eye-tracking usersScreen reader usersUsers with tremorTouch users on small screens✓ Served byMove buttons; keyboard drag modeMove buttons; Move to positionNamed buttons; position announcementsNo precision needed; undoMenus instead of long drags

ARIA & HTML spec reference

Permalink to "ARIA & HTML spec reference"
Element / attribute Valid values When to apply Common misuse
button (move up/down) — Each reorderable row Icon-only with no name
aria-label on move buttons “Move Design review up” When the visible content is an icon Same name on every row (“Up”)
button (drag handle) — Keyboard drag mode Making the whole row the handle
aria-pressed true / false Handle state while picked up Using aria-grabbed
aria-describedby ID of instructions Handle and move controls Instructions visible only on hover
aria-live="assertive" — Drag narration Polite narration that lags behind keys
role="status" — Move button confirmations Creating the region on first move
aria-grabbed, aria-dropeffect Deprecated Never Assuming they make drag accessible

Step-by-step implementation

Permalink to "Step-by-step implementation"

Step 1 — Move buttons on every row (SC 2.5.7, 2.1.1)

Permalink to "Step 1 — Move buttons on every row (SC 2.5.7, 2.1.1)"

Add Move up and Move down with names that include the item. See keyboard row reordering with move buttons.

<button type="button" data-move="up" aria-label="Move Design review up">↑</button>

Step 2 — Move to position for long lists (SC 2.1.1)

Permalink to "Step 2 — Move to position for long lists (SC 2.1.1)"

A small dialog with a number field avoids dozens of presses.

<button type="button" aria-haspopup="dialog">Move Design review to position…</button>

Step 3 — Focus follows the row (SC 2.4.3)

Permalink to "Step 3 — Focus follows the row (SC 2.4.3)"

Key rows by ID and refocus the same control after each move.

render(order); tbody.querySelector(`tr[data-id="${id}"] [data-move="${dir}"]`).focus();

Step 4 — Announce the position (SC 4.1.3)

Permalink to "Step 4 — Announce the position (SC 4.1.3)"
status.textContent = `Moved to position ${to + 1} of ${order.length}.`;

Step 5 — Optional keyboard drag mode (SC 4.1.2, 3.3.2)

Permalink to "Step 5 — Optional keyboard drag mode (SC 4.1.2, 3.3.2)"

A handle button with aria-pressed, instructions in a description and assertive narration, as in announcing drag and drop moves with live regions.

<button class="drag-handle" aria-label="Reorder Design review" aria-pressed="false" aria-describedby="dnd-help"></button>

Step 6 — Moving between groups (SC 2.5.7, 1.3.1)

Permalink to "Step 6 — Moving between groups (SC 2.5.7, 1.3.1)"

A Move to menu listing valid groups with counts. See moving rows between groups and lists.

<li role="menuitem" data-to="blocked">Blocked (2)</li>
Choosing the reordering controls Flow: every reorderable row gets move buttons; lists over about fifteen items add Move to position; grouped data adds Move to group; keyboard drag mode is an optional accelerator; deprecated attributes are removed. Choosing the reordering controlsEvery reorderable rowMove up and Move downrequired alternativeMore than ~15 rows?add Move to positionavoids long press sequencesRows belong to groups?add Move to group menunames destinationsPower users?add keyboard drag mode with narrationoptional acceleratorOld markup?remove aria-grabbed and aria-dropeffectdeprecated, unannounced

Keyboard interaction contract

Permalink to "Keyboard interaction contract"
Key Action Expected AT announcement Failure indicator
Enter / Space on Move up Move one up “Moved to position 5 of 12.” Focus lost; nothing said
Enter / Space on Move down Move one down “Moved to position 7 of 12.” Moves the wrong row
Enter on Move to position Open dialog Dialog name and field Dialog lacks a label
Space on handle Pick up “Picked up Design review, position 6 of 12…” Silent pick-up
Arrows (picked up) Move “Position 7 of 12.” Focus moves instead of the row
Space (picked up) Drop “Dropped … at position 7 of 12.” No confirmation
Escape (picked up) Cancel “Cancelled … returned to position 6.” Row stays moved

Screen reader compatibility matrix

Permalink to "Screen reader compatibility matrix"
AT + browser Move buttons Keyboard drag narration Notes
NVDA + Firefox Name and status announced Assertive messages interrupt reliably —
NVDA + Chrome Same Same —
JAWS + Chrome Same May queue briefly Keep messages short
VoiceOver + Safari Status read after button name Fast moves may skip messages Acceptable if the final message is right
TalkBack + Chrome Works with double-tap Keyboard drag needs hardware keyboard Move buttons are the main path
Narrator + Edge Works Works —

Edge cases & failure modes

Permalink to "Edge cases & failure modes"

1. Focus lost after every move

Permalink to "1. Focus lost after every move"

Diagnosis: unkeyed rendering rebuilds rows. Fix: stable keys and explicit refocus.

2. Reordering a sorted table

Permalink to "2. Reordering a sorted table"

Diagnosis: a column sort overrides manual order, so moves appear to fail. Fix: hide reorder controls while sorted, or explain how to clear the sort.

3. Narration lags behind keys

Permalink to "3. Narration lags behind keys"

Diagnosis: long or polite messages. Fix: short assertive messages; instructions only on pick-up.

4. IDs instead of names

Permalink to "4. IDs instead of names"

Diagnosis: library default announcements. Fix: custom announcements with item names.

5. Server save fails silently

Permalink to "5. Server save fails silently"

Diagnosis: optimistic reorder rolled back with no message. Fix: announce the rollback and restore focus to the row in its original place.

Keyboard row reordering with move buttons

Permalink to "Keyboard row reordering with move buttons"

Move buttons are the baseline every reorderable table needs: Move up, Move down and Move to position, with names that include the item, focus that stays with the row and a status message for each move. See keyboard row reordering with move buttons.

status.textContent = 'Already at the top.'; // boundaries are announced, not silent

aria-grabbed and aria-dropeffect deprecation

Permalink to "aria-grabbed and aria-dropeffect deprecation"

Both attributes were deprecated in ARIA 1.1 and never announced reliably. Use a handle button with aria-pressed, a description with instructions, and live narration instead. See aria-grabbed and aria-dropeffect deprecation.

<button aria-pressed="true" aria-label="Reorder Design review">⠿</button>

Announcing drag and drop moves

Permalink to "Announcing drag and drop moves"

Narrate each keypress with the smallest message that describes the change; repeat instructions only on pick-up; announce drop and cancel with the final position. See announcing drag and drop moves with live regions.

say(`Position ${pos} of ${total}` + (groupChanged ? `, in group ${group}` : ''));

Moving rows between groups and lists

Permalink to "Moving rows between groups and lists"

A Move to menu lists valid destination groups with counts and explains unavailable ones; focus follows the row or falls back predictably when the row leaves the view. See moving rows between groups and lists.

(moved ?? neighbour ?? groupHeader(to)).focus();

dnd-kit accessible sortable rows

Permalink to "dnd-kit accessible sortable rows"

dnd-kit provides a keyboard sensor and a live region; you supply names in messages, a dedicated handle and move buttons. See dnd-kit accessible sortable rows in React.

<button ref={setActivatorNodeRef} {...attributes} {...listeners} aria-label={`Reorder ${task.name}`}>⠿</button>

Cross-cutting concerns

Permalink to "Cross-cutting concerns"

Reordering and selection. Many tables combine checkboxes for selection with drag handles. Keep them in separate cells with separate names; Space on a checkbox must never start a drag. When several rows are selected, offer “Move selected rows to…” as a bulk action, and announce how many moved.

Reordering and persistence. Order is usually saved to a server. Save after each move (or debounce), announce failure with a rollback, and avoid saving partial states during a keyboard drag — save on drop.

Reordering and virtualization. In long virtualized lists, Move to position is the only practical non-drag route; keyboard dragging past the rendered window needs auto-scroll and careful testing. See accessible virtualized list patterns.

Touch. On touch screens, long-press drags are hard to discover and harder to control. Move menus work far better and should be the primary touch interaction.

Reordering approach by context Matrix of contexts — short list, long list, grouped table, virtualized list, touch screen — against move buttons, Move to position, Move to group and keyboard drag. Reordering approach by contextMove buttonsMove to positionMove to groupKeyboard dragShort listRequiredOptionalNot applicableOptionalLong listRequiredRecommendedNot applicableOptionalGrouped tableRequiredOptionalRequiredOptionalVirtualized listRequiredRecommendedIf groupedTest carefullyTouch screenRequiredRecommendedIf groupedRarely used
Move buttons appear in every row; other controls depend on the context.

Who is affected, and how

Permalink to "Who is affected, and how"
User group What breaks with drag-only reordering What works
Keyboard-only users Cannot reorder at all Move buttons, keyboard drag
Screen reader users Cannot perceive position during a drag Position announcements
Switch users Drags require many scan steps and timing One-step buttons and menus
Eye-tracking and head-pointer users Holding a “button down” while moving is tiring and imprecise Click-only controls
Users with tremor Rows dropped in the wrong place Explicit positions; undo
Touch users Long-press drags are hard to discover Menus

Writing the messages

Permalink to "Writing the messages"

The wording of reorder messages decides whether users trust the feature. Four rules cover most cases:

  1. Lead with what changed. “Position 7 of 12” rather than “Design review is now at position 7 of 12 in the task list”.
  2. Name the item when it might be ambiguous — on pick-up, drop and cancel — and not on every move.
  3. Always include the total. “Position 7” means little without “of 12”.
  4. Name groups only when they change. Repeating “in group In progress” on every press doubles the message length for no new information.

Write the messages as translation templates with plural rules from the start; reorder messages are short, frequent and easy to get grammatically wrong in other languages.

A reorder session as heard by the user Timeline of messages during one keyboard reorder: pick-up with instructions, three position messages, a group change, and the drop confirmation; then a save confirmation or rollback. A reorder session as heard by the userSpacePicked up"Picked up Design review, position 6 of 12. Arrows to move…"Down ×3Moving"Position 7 of 12", "Position 8 of 12", "Position 9 of 12"DownGroup change"Position 1 of 3, in group Blocked"SpaceDropped"Dropped Design review at position 1 of 3, group Blocked."+1 sSavedsilent on success; "Could not save order…" on failure
Six short messages for a seven-step move.

Design system integration

Permalink to "Design system integration"

Ship reordering as a capability of the table component rather than a recipe. A reorderable option should render a reorder column with move buttons and an optional handle, accept an onReorder callback, and own focus restoration, announcements and boundary messages. Message templates (moved, boundary, picked up, position, dropped, cancelled, rollback) should be localized and shared with any kanban or list components. Tokens are needed for the picked-up row’s visual state and the insertion indicator, each verified at 3:1 against the table background in light, dark and forced-colours modes. Document the keyboard contract in the component’s docs and its Storybook accessibility checks.

Testing checklist

Permalink to "Testing checklist"

Automated

Permalink to "Automated"

Keyboard

Permalink to "Keyboard"

Screen reader

Permalink to "Screen reader"

FAQ

Permalink to "FAQ"
Is drag-and-drop itself inaccessible?

No — it is fine as one way to reorder. It becomes a barrier when it is the only way. WCAG 2.5.7 asks for a single-pointer alternative, not for removing the drag.

Which is better for keyboard users: move buttons or a keyboard drag mode?

Move buttons are simpler and more predictable; keyboard drag is faster for long moves. Offer buttons always and drag mode as an accelerator if you have the resources to narrate it well.

Should reorder controls be in every row or in a toolbar?

In every row (or in each row’s action menu) so they are next to the item. A toolbar version that acts on the selected row is a useful addition for bulk moves.

How do I test reordering with a screen reader quickly?

Move one row up twice with the button and listen for two position messages with focus staying put. Then pick up a row with the handle, press Down three times, and press Escape — you should hear the position change three times and a cancellation.

Should reordering save automatically?

Usually yes — users expect the new order to stick. Save after each button move or after each drop, announce failures with a rollback, and avoid a separate Save button that keyboard users must hunt for after every change. If your domain needs explicit confirmation (for example, a published ranking), use a clear “Save order” button and announce unsaved changes.

How should undo work for reordering?

Treat a move like any other edit: Ctrl+Z (Cmd+Z) restores the previous position and announces it (“Undo: Design review back to position 6 of 12”). An Undo button in the status area for a few seconds serves pointer users. Keep the undo history per table so undo in one list never changes another.

Permalink to "Related"

← Back to Accessible Data Tables & Grid Systems