# Accessibility for Drag-and-Drop
## Table of Contents
- [Core Principles](#core-principles)
- [Keyboard Navigation](#keyboard-navigation)
- [Screen Reader Support](#screen-reader-support)
- [Alternative UI Patterns](#alternative-ui-patterns)
- [ARIA Patterns](#aria-patterns)
- [Testing Guidelines](#testing-guidelines)
## Core Principles
### The Accessibility Challenge
Drag-and-drop is inherently visual and mouse-centric, creating barriers for:
- Keyboard-only users
- Screen reader users
- Users with motor disabilities
- Touch device users with assistive tech
### Universal Design Approach
**Always provide:**
1. Full keyboard navigation
2. Clear announcements for screen readers
3. Alternative UI methods (buttons, forms)
4. Visual feedback for all states
5. Sufficient time for interactions
## Keyboard Navigation
### Standard Key Mappings
```tsx
// Recommended keyboard scheme for dnd-kit
const keyboardCoordinates = {
start: ['Space', 'Enter'], // Pick up item
cancel: ['Escape'], // Cancel drag
end: ['Space', 'Enter'], // Drop item
up: ['ArrowUp', 'w', 'W'], // Move up
down: ['ArrowDown', 's', 'S'], // Move down
left: ['ArrowLeft', 'a', 'A'], // Move left
right: ['ArrowRight', 'd', 'D'], // Move right
};
```
### Implementation with dnd-kit
```tsx
import { KeyboardSensor, useSensor } from '@dnd-kit/core';
import { sortableKeyboardCoordinates } from '@dnd-kit/sortable';
function AccessibleDragDrop() {
const keyboardSensor = useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
});
return (
{/* Draggable content */}
);
}
```
### Custom Keyboard Navigation
```tsx
// Enhanced keyboard handler with visual feedback
function useKeyboardDragDrop(items, onReorder) {
const [activeIndex, setActiveIndex] = useState(-1);
const [isGrabbed, setIsGrabbed] = useState(false);
const handleKeyDown = (e: KeyboardEvent) => {
if (!items.length) return;
switch (e.key) {
case 'Tab':
// Allow normal tab navigation when not dragging
if (!isGrabbed) return;
e.preventDefault();
break;
case ' ':
case 'Enter':
e.preventDefault();
if (activeIndex === -1) {
setActiveIndex(0);
} else if (!isGrabbed) {
// Pick up item
setIsGrabbed(true);
announceToScreenReader(`Grabbed ${items[activeIndex].label}. Use arrow keys to move.`);
} else {
// Drop item
setIsGrabbed(false);
announceToScreenReader(`Dropped ${items[activeIndex].label} at position ${activeIndex + 1}.`);
}
break;
case 'Escape':
if (isGrabbed) {
e.preventDefault();
setIsGrabbed(false);
announceToScreenReader('Drag cancelled.');
}
break;
case 'ArrowUp':
case 'ArrowDown':
e.preventDefault();
if (isGrabbed) {
const newIndex = e.key === 'ArrowUp'
? Math.max(0, activeIndex - 1)
: Math.min(items.length - 1, activeIndex + 1);
if (newIndex !== activeIndex) {
onReorder(arrayMove(items, activeIndex, newIndex));
setActiveIndex(newIndex);
announceToScreenReader(`Moved to position ${newIndex + 1} of ${items.length}.`);
}
} else {
// Navigate without dragging
const newIndex = e.key === 'ArrowUp'
? Math.max(0, activeIndex - 1)
: Math.min(items.length - 1, activeIndex + 1);
setActiveIndex(newIndex);
}
break;
}
};
return { activeIndex, isGrabbed, handleKeyDown };
}
```
## Screen Reader Support
### Live Region Announcements
```tsx
// Announcement component for screen readers
function DragDropAnnouncements({ children }) {
return (
<>
{children}
{/* Live region for announcements */}
>
);
}
// Helper function to announce
function announceToScreenReader(message: string) {
const element = document.getElementById('dnd-announcements');
if (element) {
element.textContent = message;
// Clear after announcement
setTimeout(() => {
element.textContent = '';
}, 1000);
}
}
```
### dnd-kit Accessibility Features
```tsx
import { Announcements } from '@dnd-kit/core';
// Custom announcement messages
const announcements: Announcements = {
onDragStart(id) {
return `Picked up draggable item ${id}. Press arrow keys to move, space to drop, escape to cancel.`;
},
onDragOver(id, overId) {
if (overId) {
return `Draggable item ${id} is over droppable area ${overId}.`;
}
return `Draggable item ${id} is no longer over a droppable area.`;
},
onDragEnd(id, overId) {
if (overId) {
return `Draggable item ${id} was dropped over droppable area ${overId}.`;
}
return `Draggable item ${id} was dropped.`;
},
onDragCancel(id) {
return `Dragging was cancelled. Draggable item ${id} was dropped.`;
},
};
// Use in DndContext
{/* Content */}
```
## Alternative UI Patterns
### Move Buttons Pattern
Provide explicit move buttons as an alternative to drag-and-drop.
```tsx
function AccessibleListItem({ item, index, onMove, totalItems }) {
return (