clauderic / clauderic/dnd-kit

Keyboard dragging moves by pixel offset, not between drop targets — proposal for target-based keyboard navigation

Open
#2,106 1 comment 0 reactions 0 assignees View on GitHub
a11y dom feature request triage:done
Dominant language
TypeScript
Stars
17.6k
Forks
924
Avg merge
2d 10h
Merged PRs (30d)
2

Description

### Problem

The `KeyboardSensor`'s default arrow-key behavior moves the dragged item by a fixed pixel offset (25px in `@dnd-kit/core`, `offset: 10` in `@dnd-kit/dom`). For a blind or keyboard-only user this is effectively inoperable: arrow presses don't land on droppables, so nothing meaningful is announced, and reaching a target requires sighted trial-and-error. This affects WCAG 2.1.1 (Keyboard) and undermines WCAG 2.5.7 (Dragging Movements) compliance for consumers.

The legacy packages ship `sortableKeyboardCoordinates` for sortable lists, and the docs include a "snap to the nearest droppable in the arrow's direction" example for multiple containers — but the geometric approach produces **dead presses**: if no droppable exists literally to the left/right (e.g. vertically stacked drop zones), the press silently does nothing, which is very confusing without sight.

### Proposal

Opt-in **target-based keyboard navigation**: treat enabled droppables as one ordered cycle (visual order — top-to-bottom, then left-to-right). Down/Right moves the dragged item to the *next* drop target, Up/Left to the *previous*, wrapping at the ends. Every single arrow press lands on a real target, so collision/announcement events fire on every press and the interaction is fully non-visual-friendly.

We've shipped this in production (an LMS matching / drag-to-match question) as a custom `KeyboardCoordinateGetter` against `@dnd-kit/core` 6.3.1 and verified it with VoiceOver. Reference implementation (~40 lines):

```ts
import {
closestCorners,
getFirstCollision,
KeyboardCode,
type DroppableContainer,
type KeyboardCoordinateGetter,
} from '@dnd-kit/core';

const forwardCodes: string[] = [KeyboardCode.Down, KeyboardCode.Right];
const backwardCodes: string[] = [KeyboardCode.Up, KeyboardCode.Left];

export const cycleDropZones: KeyboardCoordinateGetter = (
event,
{context: {active, droppableRects, droppableContainers, collisionRect}}
) => {
const forward = forwardCodes.includes(event.code);
if (!forward && !backwardCodes.includes(event.code)) return undefined;
event.preventDefault();
if (!active || !collisionRect) return undefined;

// Visual order: top-to-bottom, then left-to-right
const ordered: DroppableContainer[] = droppableContainers
.getEnabled()
.filter((entry) => !entry.disabled && droppableRects.get(entry.id))
.sort((a, b) => {
const ra = droppableRects.get(a.id)!;
const rb = droppableRects.get(b.id)!;
return ra.top - rb.top || ra.left - rb.left;
});

if (ordered.length === 0) return undefined;

// The zone closest to the item's current position is where it "is" now
const collisions = closestCorners({
active,
collisionRect,
droppableRects,
droppableContainers: ordered,
pointerCoordinates: null,
});
const currentId = getFirstCollision(collisions, 'id');
const currentIndex = ordered.findIndex((entry) => entry.id === currentId);

const nextIndex =
currentIndex === -1
? forward ? 0 : ordered.length - 1
: (currentIndex + (forward ? 1 : -1) + ordered.length) % ordered.length;

const nextRect = droppableRects.get(ordered[nextIndex].id);
return nextRect ? {x: nextRect.left, y: nextRect.top} : undefined;
};
```

I'm happy to contribute a PR porting this behavior to the new `KeyboardSensor` in `@dnd-kit/dom`.

### Question for maintainers

Would you prefer this as a `KeyboardSensor` option (e.g. `navigation: 'offset' | 'targets'`) or as a plugin in the new plugin architecture? I'll shape the PR accordingly.

Contributor guide

Open the contributing guide

Research direction

Start with the new KeyboardSensor in @dnd-kit/dom and the plugin architecture mentioned in the proposal; compare them with the existing sortableKeyboardCoordinates behavior in the legacy packages. Confirm how enabled droppables, collision rectangles, and keyboard announcements are exposed before deciding where the opt-in belongs. Done means the API and target-navigation behavior are agreed and covered by relevant keyboard-navigation tests.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
accessibility, frontend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.