Moving Rows Between Groups and Lists
Permalink to "Moving Rows Between Groups and Lists"Grouped tables and multi-list layouts — tasks grouped by status, leads grouped by stage, files grouped by folder — invite a second kind of drag: moving a row from one group to another. Changing a task from “In progress” to “Blocked” by dragging it across the boundary is fast with a mouse and impossible without one. Unlike simple reordering, the move changes the row’s meaning, so the accessible alternative must name destinations clearly and handle the fact that the row may vanish from the user’s current view.
This guide belongs to row drag & drop reordering and builds on accessible group header rows.
Spec reference
Permalink to "Spec reference"- SC 2.5.7 Dragging Movements (AA) — a single-pointer alternative is required; a Move to… menu is the usual one.
- SC 2.1.1 Keyboard (A) — moving between groups must work from the keyboard.
- SC 2.4.3 Focus Order (A) — after a move, focus must be somewhere logical even if the row is now in a collapsed or filtered-out group.
- SC 4.1.3 Status Messages (AA) — “Moved to Blocked, position 2 of 5” is a status message.
- SC 1.3.1 Info and Relationships (A) — group membership must be programmatic (group header rows,
aria-levelin treegrids, or separate labelled lists).
When to use — and when not to
Permalink to "When to use — and when not to"Use a Move to menu whenever rows can change group by drag. It is often better than drag for everyone: it lists destinations that may be off-screen, it avoids accidental drops, and it works on touch devices without precise drags.
If the group is a field of the record (status, owner, stage), consider whether an editable field is clearer than “moving”. Editing “Status” in a select inside the row is a familiar form interaction that needs no special pattern, and the row then moves as a side effect. The misapplication is a drag-only kanban where the only way to change a status is to move a card across columns.
Annotated code example
Permalink to "Annotated code example"<tr data-id="t7" data-group="in-progress">
<th scope="row">Design review</th>
<td>
<!-- Menu button pattern (SC 4.1.2); name includes the item (SC 2.4.6) -->
<button type="button" aria-haspopup="menu" aria-expanded="false" aria-controls="m-t7">
Move Design review to…
</button>
<ul role="menu" id="m-t7" hidden>
<li role="menuitem" data-to="todo">To do (4)</li>
<li role="menuitem" data-to="blocked">Blocked (2)</li>
<li role="menuitem" data-to="done">Done (11)</li>
<!-- Unavailable destinations explained, not silently omitted -->
<li role="menuitem" aria-disabled="true" aria-describedby="why-archived">Archived</li>
</ul>
<span id="why-archived" hidden>Archiving needs approval from a project owner.</span>
</td>
</tr>
function moveToGroup(id, to) {
const row = findRow(id);
const neighbour = row.nextElementSibling ?? row.previousElementSibling; // fallback focus target
model.move(id, to);
render();
const moved = document.querySelector(`tr[data-id="${id}"] [aria-haspopup="menu"]`);
// If the destination group is collapsed or filtered out, the row is not rendered (SC 2.4.3)
(moved ?? neighbour?.querySelector('th, button') ?? groupHeader(to)).focus();
const { pos, size, label } = model.position(id);
status.textContent = `Moved Design review to ${label}, position ${pos} of ${size}.` +
(moved ? '' : ` ${label} is collapsed.`); // SC 4.1.3
}
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Event | Expected announcement | AT deviations |
|---|---|---|
| Enter on Move to… | “Move Design review to, menu, 4 items” | VoiceOver: “menu, 4 items” after the name |
| Arrow to “Blocked (2)” | “Blocked (2), menu item, 2 of 4” | — |
| Enter | Menu closes; focus on the moved row in Blocked; “Moved Design review to Blocked, position 3 of 3.” | JAWS may read the group header row first |
| Destination collapsed | Focus to former neighbour; “…Blocked is collapsed.” | — |
| Keyboard drag across boundary | “Position 1 of 3, in group Blocked” | — |
Integration context
Permalink to "Integration context"Group header rows and their expand and collapse behaviour are covered in collapsing row groups with aria-expanded. The menu itself follows row action menus with the menu button pattern. For a keyboard drag that crosses groups, use the narration from announcing drag and drop moves with live regions, adding the group name whenever it changes.
Gotchas
Permalink to "Gotchas"Group counts. Moving a row changes two group counts. Update the visible counts in both headers, but do not announce both; the destination and position are enough.
Validation rules. Some moves are invalid (a task cannot be Done while subtasks are open). Show such destinations as disabled with a reason rather than hiding them, so users understand why.
Bulk moves. Users often need to move many selected rows. Put Move to… in the bulk action toolbar as well, and announce “Moved 6 tasks to Blocked.”
Framework notes
Permalink to "Framework notes"With dnd-kit, multiple containers are modelled as separate SortableContexts; its keyboard sensor moves across containers with arrow keys, and your onDragOver announcement should include the container name. With Angular CDK, connect lists with cdkDropListConnectedTo and use transferArrayItem on drop. In both cases, add the Move to menu — neither library provides a non-drag alternative.
// dnd-kit: include the container (group) in drag-over announcements
onDragOver: ({ active, over }) => over
? `Position ${indexIn(over)} of ${sizeOf(groupOf(over))}, in group ${groupOf(over)}`
: 'Not over a group.',
Testing checklist
Permalink to "Testing checklist"Design system notes
Permalink to "Design system notes"Model groups as data — { id, label, count, accepts(row) } — and generate the Move to menu, the drag drop targets and the announcements from the same model. When a destination’s rules change, every surface updates together.
FAQ
Permalink to "FAQ"Is a select element better than a Move to menu?
If the group is a field on the record, an inline select for that field is often the clearest option. A Move to menu suits cases where moving is an action rather than an edit, or where moves have side effects.
Should the moved row stay focused if its new group is far away in the table?
Yes, if it is rendered — scroll it into view and focus it. Users usually want to continue with that row. If they would rather stay where they were, a “Stay here after moving” preference helps power users.
How should moves be undone?
Offer Undo in the status area for a few seconds and as a keyboard shortcut, and announce the undo result. See the undo guidance for destructive row actions.
What about moving rows between two separate tables?
Treat each table as a group: the Move to menu lists the other table by name, and the announcement says “Moved to Archive table, position 1 of 8”.
Related
Permalink to "Related"- Move buttons for reordering — moves within a group
- Accessible group header rows — the groups themselves
- Confirming and undoing destructive row actions — undo after a move