Drag and Drop List Reorder
Machine-coding brief for list reorder — pointer drag, keyboard move, optimistic order, and accessible announcements.
- machine-coding
- interview
- react
- dnd
Problem statement
Build a reorderable list: users drag items to a new index (and ideally can reorder with keyboard). Interviewers score index math, visual feedback, and whether you treat DnD as progressive enhancement over keyboard — not whether you pull in a heavy library (ask if libraries are allowed).
Requirements
Must have
- Render ordered list from data
- Drag handle or whole row draggable
- Drop target indicator (line or gap highlight)
- On drop, new order via
onChange(nextItems) - Works with mouse / touch pointer events
- Keyboard alternative: move item up/down with buttons or keys
Should have
aria-liveannouncement after reorder- Disable drag on interactive children (links, buttons) via handle-only drag
- Smooth transform preview without layout thrash
Nice to have
- Multi-column (kanban) — usually separate problem
- Auto-scroll when dragging near edges
- Persist order
Planning (5 minutes out loud)
- HTML5 DnD vs pointer events — HTML5 is faster to demo; pointer is more controllable
- Controlled list — parent owns order
- Drag index + hover index during gesture
- MVP — HTML5 reorder + up/down buttons; polish preview
- A11y is not optional — buttons for move if timeboxed
Architecture
ReorderList
├── ReorderItem
│ ├── DragHandle
│ ├── Content
│ └── MoveUp / MoveDown
└── live region
Data model
type Item = { id: string; label: string };
type ReorderListProps = {
items: Item[];
onChange: (items: Item[]) => void;
getItemLabel?: (item: Item) => string;
};
function moveItem<T>(list: T[], from: number, to: number): T[] {
if (from === to || from < 0 || to < 0 || from >= list.length || to >= list.length) {
return list;
}
const next = list.slice();
const [spliced] = next.splice(from, 1);
next.splice(to, 0, spliced);
return next;
}
Implementation sketch — HTML5 DnD
function ReorderList({ items, onChange }: ReorderListProps) {
const dragIndex = useRef<number | null>(null);
const [overIndex, setOverIndex] = useState<number | null>(null);
const [announce, setAnnounce] = useState("");
function onDragStart(index: number) {
dragIndex.current = index;
}
function onDragOver(e: React.DragEvent, index: number) {
e.preventDefault(); // allow drop
setOverIndex(index);
}
function onDrop(index: number) {
const from = dragIndex.current;
if (from == null) return;
const next = moveItem(items, from, index);
onChange(next);
setAnnounce(
`Moved ${items[from].label} to position ${index + 1} of ${items.length}`
);
dragIndex.current = null;
setOverIndex(null);
}
function move(from: number, delta: number) {
const to = from + delta;
const next = moveItem(items, from, to);
if (next === items) return;
onChange(next);
setAnnounce(`Moved ${items[from].label} to position ${to + 1}`);
}
return (
<>
<ul className="reorder-list" aria-label="Reorderable list">
{items.map((item, index) => (
<li
key={item.id}
draggable
onDragStart={() => onDragStart(index)}
onDragOver={(e) => onDragOver(e, index)}
onDrop={() => onDrop(index)}
onDragEnd={() => setOverIndex(null)}
data-over={overIndex === index || undefined}
>
<span className="handle" aria-hidden>
⋮⋮
</span>
{item.label}
<button type="button" aria-label={`Move ${item.label} up`} onClick={() => move(index, -1)}>
Up
</button>
<button type="button" aria-label={`Move ${item.label} down`} onClick={() => move(index, 1)}>
Down
</button>
</li>
))}
</ul>
<div className="sr-only" aria-live="assertive" aria-atomic="true">
{announce}
</div>
</>
);
}
Pointer-based sketch (more control)
// onPointerDown on handle → set draggingId, pointer capture
// onPointerMove → hit-test list items via elementFromPoint or bounding boxes
// onPointerUp → commit moveItem
// preview: transform: translateY during drag (optional)
Hit-testing midpoints: if pointer Y is above item center, insert before; else after. Collapse to target index carefully to avoid flicker.
Accessibility essentials
- Never keyboard-trap only in drag mode
- Provide Move up/down (or Alt+Arrow) always
- Live region after reorder (
assertiveshort message) - Drag handle: if whole row is draggable, ensure buttons still clickable (
draggableon handle only is cleaner) - Visible drop indicator not color-only
Performance notes
- During drag, avoid
onChangeevery pixel — update UI local state; commit on drop - Large lists: virtualize + library; mention complexity of hit testing
- Prefer
transformfor ghost preview over top/left layout
Footguns
- Forgetting
preventDefaulton dragover — drop never fires - Using index as React key — identity bugs after reorder
- Stale closure on dragIndex in React state instead of ref mid-gesture
- Touch: HTML5 DnD is weak on mobile — pointer events or library
- Nested interactive controls accidentally starting drags
Interview out-loud answer
I’d keep the list controlled and implement
moveItem(from, to). HTML5 drag-and-drop is fine for desktop MVP with dragover preventDefault and a drop highlight. I’d always ship Move up/down buttons and an aria-live announcement. If they need mobile polish, I’d switch to pointer events with hit-testing. Kanban multi-list is the same index math plus a target list id.