Offering keyboard and screen reader alternatives
Drag and drop feels intuitive for sighted mouse users: grab an element, move it, release it. For keyboard users and screen reader users, this interaction usually does not exist at all, because it is built exclusively on mouse and touch events, without a single one of the involved elements being reachable via Tab or activatable via Enter. Offering product sorting in the admin or a wishlist order exclusively through drag and drop effectively locks an entire user group out of the feature.
Table of Contents
- 1. Why pure drag and drop is unusable for keyboard and screen reader users
- 2. From the deprecated aria-grabbed to the modern ARIA grab pattern
- 3. Up/down buttons as a robust fallback instead of arrow keys alone
- 4. Practical example: accessible wishlist sorting in Hyvä
- 5. Admin product sorting: the same pattern in the backend
- 6. Image upload order: a second typical use case
- 7. Don't forget focus management after a move
- 8. Test checklist for accessible sortable lists
- 9. Limits of the pattern and when a simpler solution is enough
- 10. Summary
- 11. FAQ
1. Why pure drag and drop is unusable for keyboard and screen reader users
Classic drag and drop implementations rely on a chain of pointer events: mousedown or pointerdown on the element being moved, a sequence of mousemove events while dragging, and finally mouseup on release over the target area. None of these events has a keyboard equivalent. A user navigating with Tab can focus the element, but there is no key that simulates grabbing, no key that simulates dragging, and no key that simulates dropping.
For screen reader users, a second problem compounds this: even if a keyboard handler were retrofitted, the feature remains incomprehensible unless the screen reader announces that an element is grabbable, what position it currently occupies, and when a move has succeeded. Visual feedback like a shadow under the dragged element or a highlighted drop zone is entirely invisible to blind users unless it is additionally translated into ARIA attributes.
2. From the deprecated aria-grabbed to the modern ARIA grab pattern
The WAI-ARIA specification used to include the attributes aria-grabbed and aria-dropeffect, explicitly designed for drag and drop states. Both attributes have since been marked deprecated, because they were inconsistently supported across screen readers in practice, and developers often built implementations that were technically annotated correctly but still unusable. The current recommendation in the WAI-ARIA Authoring Practices drops these attributes in favor of a pattern built from live regions, aria-describedby, and explicit keyboard handlers.
The modern pattern works like this: every sortable element is marked up as a focusable button or a role="button" element with tabindex="0". An aria-describedby points to a hidden help text explaining keyboard operation, such as arrow keys to move and Enter to confirm. An aria-live region announces every successful position change, so screen reader users always know where the element currently sits.
<!-- Sortable element with the modern ARIA grab pattern -->
<li>
<button type="button"
class="sortable-item"
aria-describedby="sortable-hint"
@keydown.up.prevent="moveUp(item.id)"
@keydown.down.prevent="moveDown(item.id)">
<?= $block->escapeHtml($item->getName()) ?>
</button>
</li>
<span id="sortable-hint" class="sr-only">
Arrow up or arrow down moves the entry within the list.
</span>
<!-- Live region announces every position change -->
<div aria-live="polite" class="sr-only" x-text="liveMessage"></div>
3. Up/down buttons as a robust fallback instead of arrow keys alone
Arrow key handlers alone solve the keyboard operability problem, but not the discoverability problem. A sighted keyboard user who has never worked with this component before has no visual clue that arrow keys have a function here, unless a visible hint text exists. Visible up/down buttons next to every list entry solve this more reliably, because they map to exactly the same function while requiring no hidden knowledge of key combinations.
These buttons are also a lifeline for users with motor impairments who can operate a mouse but cannot perform precise dragging motions, for example due to tremor or reduced fine motor control. A simple click on an arrow button requires far less motor precision than a controlled drag across several hundred pixels.
<!-- Visible up/down buttons alongside drag and drop,
not hidden as a mere keyboard-only workaround -->
<li class="flex items-center justify-between gap-4 py-2">
<span><?= $block->escapeHtml($item->getName()) ?></span>
<div class="flex gap-1">
<button type="button"
class="min-h-[24px] min-w-[24px]"
aria-label="<?= $block->escapeHtmlAttr(__('Move %1 up', $item->getName())) ?>"
@click="moveUp(item.id)">↑</button>
<button type="button"
class="min-h-[24px] min-w-[24px]"
aria-label="<?= $block->escapeHtmlAttr(__('Move %1 down', $item->getName())) ?>"
@click="moveDown(item.id)">↓</button>
</div>
</li>
4. Practical example: accessible wishlist sorting in Hyvä
A wishlist with several dozen products benefits strongly from a freely orderable sequence, for example to set priorities before a purchase. A pure drag and drop implementation using a JavaScript library like Sortable.js looks elegant at first glance, but remains unusable for anyone who cannot drag precisely with a mouse or who operates the wishlist keyboard-only.
The accessible implementation combines both worlds in a single Alpine.js component: drag and drop remains available as a quick path for mouse users, while the same moveUp/moveDown method is called from both the visible buttons and the arrow key handlers. After every move, the new position is announced via an aria-live region and the order is persisted to the server via an AJAX request, without triggering a full page reload.
// wishlist-sort.js: one method shared by drag-drop, buttons, and keyboard
function wishlistSort(items) {
return {
items,
liveMessage: '',
moveUp(id) {
const index = this.items.findIndex(i => i.id === id);
if (index <= 0) return;
this.swap(index, index - 1);
},
moveDown(id) {
const index = this.items.findIndex(i => i.id === id);
if (index >= this.items.length - 1) return;
this.swap(index, index + 1);
},
swap(a, b) {
[this.items[a], this.items[b]] = [this.items[b], this.items[a]];
this.liveMessage = `${this.items[b].name} is now position ${b + 1} of ${this.items.length}`;
this.persistOrder();
},
async persistOrder() {
await fetch('/wishlist/reorder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order: this.items.map(i => i.id) }),
});
},
};
}
5. Admin product sorting: the same pattern in the backend
In the Magento admin, the same problem shows up in product sorting within a category. The default grid uses drag and drop handles moved with a mouse. Editors who rely on keyboard operation for health reasons, or power users who prefer fast keyboard workflows, often cannot meaningfully use the default UI.
An accessible addition to the admin area does not need to replace the entire sort widget, but can function as an extra input option: a number field per row where a target position can be entered directly, combined with the same up/down buttons as on the frontend. This addition can be retrofitted as its own UI component within the existing grid with manageable effort, while also reducing error-proneness for very long product lists, where precise dragging across many screens' worth of scrolling is tedious regardless.
6. Image upload order: a second typical use case
The product image gallery in the Magento admin also uses drag and drop to set the display order of images, including which one is the base image. The same problem as with category sorting shows up here again, with the added complication that images are often only distinguishable by filename, which offers screen reader users little orientation without meaningful alt text.
An accessible variant should therefore not only offer keyboard buttons for moving images, but also assign every image in the list a short, generated or manually maintained label that appears both in the live region and in the aria-label of the respective button, for example 'Image 2 of 5, product photo front view' instead of just 'Image 2 of 5'.
<!-- Image list with a descriptive label instead of just a position number -->
<li>
<img src="..." alt="" class="w-16 h-16 object-cover">
<span class="sr-only">Image 2 of 5, product photo front view</span>
<button type="button" aria-label="Image 2 of 5, product photo front view, move up">↑</button>
</li>
7. Don't forget focus management after a move
A frequently overlooked mistake when implementing up/down buttons: after a click, focus disappears because the element is moved to a new DOM position and re-rendered in the process, which causes the browser to silently lose focus and reset it to body. For keyboard users this means they have to Tab back to the correct position after every single move, which slows operation down considerably.
The fix is to explicitly assign focus after every reorder operation to the same logical button the user just activated, even though its position in the list has changed. In Alpine.js this can be implemented via a $nextTick callback and a stable ref or id mapping per element, so focus follows the element instead of staying pinned to its original screen position.
// Assign focus to the moved element, not the old position
swap(a, b) {
const movedId = this.items[a].id;
[this.items[a], this.items[b]] = [this.items[b], this.items[a]];
this.$nextTick(() => {
document.querySelector(`[data-item-id="${movedId}"] button`)?.focus();
});
}
8. Test checklist for accessible sortable lists
Before shipping a sortable list, a short, repeatable test pass pays off: put the mouse away entirely and reorder the whole list keyboard-only, checking whether every move is announced sensibly while an active screen reader like NVDA or VoiceOver is running. It is also worth checking whether focus stays at a predictable spot after every action instead of jumping back to body.
A third test concerns persistence: after reordering via keyboard, the same order must be saved as after reordering via mouse, through the same backend endpoint. Implementing the keyboard variant as a separate code path easily creates inconsistencies, where keyboard sorting works visually but gets discarded again after a page reload.
9. Limits of the pattern and when a simpler solution is enough
Not every sortable list needs the full complexity of the ARIA grab pattern. For very short lists with at most three or four entries, it can be simpler and more robust to skip drag and drop entirely and work exclusively with numbered dropdown fields per row, where the target position is chosen directly. This reduces implementation complexity considerably and is equally accessible across all input methods, without needing live regions or focus management at all.
For very long lists with hundreds of entries, such as a large product catalog in the admin, both drag and drop and pure arrow key movement hit practical limits. Here a direct number field for the target position combined with server-side sort persistence is usually the more robust and faster to operate solution, regardless of the input method used.
| Approach | Keyboard operable | Screen reader understandable | Recommended for |
|---|---|---|---|
| Pure drag and drop (mouse/touch) | No | No | Never as the only solution |
| ARIA grab pattern with arrow keys | Yes | Yes, with live region | Medium lists, 5 to 50 entries |
| Visible up/down buttons | Yes | Yes, via aria-label | All list lengths, best discoverability |
| Numbered field per row | Yes | Yes, natively | Very long lists, admin grids |
| Legacy aria-grabbed / aria-dropeffect | Partially | Inconsistent | No longer recommended, deprecated |
Mironsoft
WCAG audits, accessible Magento shops, and training
Not sure whether the shop is actually accessible?
We audit existing Magento shops against WCAG 2.2, fix concrete barriers in the Hyvä frontend, and train teams so accessibility stays anchored in the development process for good.
WCAG Audit
Systematically review the shop against WCAG 2.2 AA, with a prioritized issue list.
Fixing Barriers
Concrete implementation: keyboard operability, screen reader support, contrast, forms.
Team Training
Raise developer and editor awareness for accessible implementation day to day.
10. Summary
Accessible Drag and Drop: The Essentials at a Glance
Core problem
Pure drag and drop relies exclusively on pointer events and has no native keyboard equivalent, which locks out keyboard and screen reader users entirely.
Modern pattern
aria-grabbed and aria-dropeffect are deprecated, the current recommendation uses focusable elements, arrow key handlers, and aria-live announcements.
Robust fallback
Visible up/down buttons next to every list entry solve both keyboard and discoverability problems and additionally help mouse users with motor impairments.
Practical detail
After every move, focus must be explicitly assigned to the moved element, otherwise it silently jumps back to body.