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"- Semantic HTML table construction — reordering never changes the need for correct headers and row headers.
- ARIA live regions for dynamic data, especially choosing between polite and assertive aria-live regions.
- Focus management in single-page apps, because frameworks rebuild rows on reorder.
- For grouped tables, row grouping & aggregation.
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>
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.
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:
- Lead with what changed. “Position 7 of 12” rather than “Design review is now at position 7 of 12 in the task list”.
- Name the item when it might be ambiguous — on pick-up, drop and cancel — and not on every move.
- Always include the total. “Position 7” means little without “of 12”.
- 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.
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.
Related
Permalink to "Related"- Spreadsheet-style editing grids — the fill handle, another drag
- Row grouping & aggregation — the groups rows move between
- Row actions & context menus — where move commands live
- Bulk selection & batch actions — moving many rows at once
- Column resizing, reordering & freezing — the column-side equivalent