OMAR
Field NotesCV
dnd-kit: Drag and Drop That Works Without a Mouse
← All Notes
Frontend04 August 2026 · 3 min read

dnd-kit: Drag and Drop That Works Without a Mouse

Most drag-and-drop implementations are unusable by keyboard and silent to screen readers. This one ships both, and it is lighter than what it replaces.

Reorderable lists are everywhere: task boards, form builders, gallery ordering, dashboard widgets. Almost none of them can be operated without a mouse, and almost none of them say anything to a screen reader while an item moves. It is treated as an acceptable exclusion, and it is not one — it is just the default of the libraries people reached for.

Sortable list, keyboard included

import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'
import { SortableContext, arrayMove, sortableKeyboardCoordinates } from '@dnd-kit/sortable'

const sensors = useSensors(
  useSensor(PointerSensor),
  useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
)

<DndContext sensors={sensors} onDragEnd={({ active, over }) => {
  if (over && active.id !== over.id) {
    setItems((items) => arrayMove(items, items.indexOf(active.id), items.indexOf(over.id)))
  }
}}>
  <SortableContext items={items}>
    {items.map(id => <SortableRow key={id} id={id} />)}
  </SortableContext>
</DndContext>

That KeyboardSensor is the whole argument. Tab to an item, press space to lift it, arrow keys to move, space to drop, escape to cancel. It is the same interaction pattern operating systems have used for decades, and it costs two lines.

Say what happened

<DndContext accessibility={{
  announcements: {
    onDragStart: ({ active }) => `Picked up ${active.id}`,
    onDragOver: ({ active, over }) => over && `${active.id} is over position ${over.data.current.sortable.index + 1}`,
    onDragEnd: ({ active, over }) => over && `${active.id} dropped at position ${over.data.current.sortable.index + 1}`,
  },
}}>

These go into a live region. Without them a screen-reader user hears silence during the one interaction where feedback matters most. Write them in the user's language — on bilingual projects this is a string that needs translating like any other.

It does not use HTML5 drag events

Which is why it works on touch, why the drag preview is a React component you style instead of a browser-generated ghost image, and why the behaviour is the same across browsers rather than the same only on the one you tested.

The honest caveat

Drag is a fine-motor interaction. Even done perfectly, some people cannot use it. Anywhere ordering actually matters, keep a second route to the same outcome — a "move up / move down" control in a row menu, or a numeric position field. dnd-kit makes the drag path good; it does not remove your obligation to have a non-drag path.

Why I use it on admin work

Client dashboards are where reordering lives, and admin tools are where accessibility gets deprioritised because "it is only for staff". Staff include people who use screen readers. The keyboard path here is close to free, so there is no argument left for skipping it.

Resources

ReactAccessibilityInteraction

Need this built properly?

I build secure, fast, bilingual platforms for clients across Egypt, Saudi Arabia, the UAE and Kuwait.

Keep Reading