Announcing Drag and Drop Moves with Live Regions

Permalink to "Announcing Drag and Drop Moves with Live Regions"

Keyboard drag mode lets users pick up a row with Space, move it with arrow keys and drop it with Space again. Visually, the row floats and a placeholder shows where it will land. Non-visually, every one of those changes has to be narrated — otherwise a screen reader user presses Down three times and has no idea whether the row moved, where it is, or whether it can be dropped there. The narration is short, frequent and tightly coupled to keypresses, which makes it different from most live region use.

This guide belongs to row drag & drop reordering. It assumes a drag handle built as described in aria-grabbed and aria-dropeffect deprecation.

Spec reference

Permalink to "Spec reference"
  • SC 4.1.3 Status Messages (AA) — each position change is a status message; the live region makes it perceivable without moving focus.
  • SC 3.3.2 Labels or Instructions (A) — the keys for drag mode are instructions and must be available, typically via aria-describedby on the handle and repeated on pick-up.
  • SC 2.1.1 Keyboard (A) and SC 2.5.7 Dragging Movements (AA) — drag mode serves keyboard users; move buttons still serve single-pointer users.
  • ARIA live region semantics — aria-live="assertive" interrupts current speech, which is appropriate when each message answers a keypress the user just made.
Narration for one keyboard drag Sequence between user, drag handle and live region: Space picks up and is narrated with keys; two Down presses are narrated as positions; an invalid target is narrated; Space drops and the final position is narrated. Narration for one keyboard dragUserHandleLive regionSpace"Picked up Design review, position 6of 12. Arrows to move, Space to drop,Escape to cancel."Down"Position 7 of 12"Down"Position 8 of 12, in group Blocked"Space"Dropped Design review at position 8of 12, group Blocked."
Every keypress gets exactly one short message.

When to use — and when not to

Permalink to "When to use — and when not to"

Use narration for any keyboard drag: reordering rows, moving rows between groups, moving cards between columns. Keep each message to the information that changed — position, and group or target if it changed.

Do not narrate pointer drags in the same way. Screen reader users rarely drag with a mouse, and announcing every mousemove-driven target change floods the speech queue for users of screen readers with a mouse (some low-vision users do this). For pointer drags, announce only the drop.

The misapplication: one long message per move, repeating the instructions every time (“Position 7 of 12. Use arrow keys to move. Press Space to drop…”). By the third keypress the user is still hearing the first instructions.

Repeating everything versus saying what changed Comparison: repeated full messages lag behind key presses and bury the position; short change-only messages keep pace. Repeating everything versus saying what changed✗ Full sentence on every move"Design review, position 7 of 12, use arrows…"Speech lags several presses behindInstructions repeated 10 timesGroup change not distinguished✓ Short message on every move"Position 7 of 12"Speech keeps up with the keysInstructions once, on pick-up"Position 8, in group Blocked" only when thegroup changes

Annotated code example

Permalink to "Annotated code example"
// One region for all drags, rendered on load (SC 4.1.3)
const live = document.getElementById('dnd-live'); // <p aria-live="assertive" class="visually-hidden">

const say = (msg) => {
  live.textContent = '';                         // clear so identical text ("Position 7 of 12") re-announces
  requestAnimationFrame(() => { live.textContent = msg; });
};

function onPickUp(item) {
  say(`Picked up ${item.name}, position ${item.pos} of ${total}. ` +
      `Arrows to move, Space to drop, Escape to cancel.`);   // instructions once (SC 3.3.2)
}

let lastGroup = null;
function onMove(item, target) {
  if (!target.valid) return say(`Cannot drop here. ${target.reason}.`);
  const groupChanged = target.group !== lastGroup;
  lastGroup = target.group;
  say(`Position ${target.pos} of ${target.total}` + (groupChanged ? `, in group ${target.group}` : ''));
}

function onDrop(item, target) {
  say(`Dropped ${item.name} at position ${target.pos} of ${target.total}` +
      (target.group ? `, group ${target.group}.` : '.'));
  setTimeout(() => { live.textContent = ''; }, 3000);    // leave a clean region for next time
}

function onCancel(item) {
  say(`Cancelled. ${item.name} returned to position ${item.origin} of ${total}.`);
}

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Event Expected announcement AT deviations
Space on handle “Picked up Design review, position 6 of 12. Arrows to move…” JAWS may also say “pressed”
Down (×3, fast) Each press interrupts: “Position 7…”, “Position 8…”, “Position 9 of 12” VoiceOver may skip intermediate ones — acceptable
Down into another group “Position 9 of 12, in group Blocked” —
Down at the end “Cannot move further. Last position.” —
Escape “Cancelled. Design review returned to position 6 of 12.” —
Characters spoken per move Bar chart of message length in characters: a repeated full instruction message per move is about 110 characters, a position-only message about 17, and a position with a group change about 38. Characters spoken per moveFull message every movelags behind fast key pressesPosition onlykeeps pacePosition + group changeonly when the group changes
Short messages keep pace with the arrow keys.

Integration context

Permalink to "Integration context"

Narration is half of keyboard dragging; the handle’s state and description are covered in aria-grabbed and aria-dropeffect deprecation. Moving between groups adds group names to the messages, as in moving rows between groups and lists. For the general rules on politeness, see choosing between polite and assertive aria-live regions.

Gotchas

Permalink to "Gotchas"

Identical consecutive messages. Moving back and forth between positions 7 and 8 produces repeated strings; clear the region before each update or some readers stay silent.

Focus following the item. If focus stays on a handle that has been moved in the DOM, some frameworks destroy and recreate it. Keep the element instance or refocus it after each move, or the narration continues while focus is lost.

Visual-only placeholders. The insertion line during a drag must not be the only indication of position for sighted keyboard users — keep the moved row itself highlighted with a visible focus indicator.

Framework notes

Permalink to "Framework notes"

dnd-kit exposes announcements and screenReaderInstructions options on DndContext; supply item names instead of the default IDs (“Picked up draggable item 3” is a common shipped bug). Angular CDK emits cdkDropListDropped and drag events you can feed to LiveAnnouncer with 'assertive'. Pragmatic drag and drop (Atlassian) ships a live region helper in @atlaskit/pragmatic-drag-and-drop-live-region.

// Angular CDK
drop(e: CdkDragDrop<Task[]>) {
  moveItemInArray(this.tasks, e.previousIndex, e.currentIndex);
  this.announcer.announce(`Dropped ${e.item.data.name} at position ${e.currentIndex + 1} of ${this.tasks.length}.`, 'assertive');
}

Testing checklist

Permalink to "Testing checklist"

Design system notes

Permalink to "Design system notes"

Publish message templates for drag narration — pickUp, move, moveToGroup, invalid, drop, cancel — with translation keys and plural rules, and wire every drag-and-drop integration through them. Consistent wording across products is what lets screen reader users build muscle memory.

FAQ

Permalink to "FAQ"
Why assertive rather than polite?

Each message responds to a keypress the user just made. Polite messages wait for current speech, so they would lag several presses behind and describe positions the item has already left.

Should the instructions be repeated on every pick-up?

Yes, briefly, on pick-up only. Users who know them can interrupt speech by pressing an arrow key.

What if the list is very long?

Announce position and total, and consider Page Up and Page Down to move by ten. Mention those keys in the instructions.

Should the drop be confirmed by the server before announcing?

Announce the drop immediately, then announce a rollback if the save fails (“Could not save order. Design review moved back to position 6.”). Waiting for the server makes the interaction feel broken.

Should the live region be inside the table?

No. Put it outside the table, near the end of the body, so it is never re-rendered with the rows. A region inside a row that moves is destroyed and recreated mid-drag, and its messages are lost.

Permalink to "Related"

← Back to Row Drag & Drop Reordering