Representative interview topic

Frontend Interview: How Do You Build an Accessible Drag-and-Drop Sortable List?

FrontendHard
Offer.cc Editorial TeamPublished Updated

Question

Build a sortable task list that supports mouse and touch dragging without making dragging the only way to reorder. Explain the state model, stable-ID reorder operation, keyboard and single-pointer alternatives, focus and screen-reader behavior, cancellation, persistence failures, and an accessibility verification plan.

Prompt and Applicable Context

Build a task list whose items can be reordered. Mouse and touch users may drag an item, but every reorder must also be possible with the keyboard and with clicks or taps that do not require a dragging movement. A screen-reader user must learn which item moved and its new position. The design must preserve focus, support cancellation, avoid losing or duplicating items, and handle a failed save without leaving the visible order ambiguous.

Assume the base list is rendered in full, each item has a stable unique ID, and only one item moves at a time. Reordering within a virtualized list, moving several selected items, and resolving concurrent edits are follow-ups. The framework is not the main decision: React may render the view, but the interview tests input modeling, accessible semantics, state ownership, and verification.

Dragging is an enhancement, not the business operation. The business operation is “move item X to final position Y.” Pointer drag, keyboard commands, and visible move controls should all invoke that same operation. This separation prevents three input paths from producing three subtly different orders.

What the Interviewer Evaluates

The first signal is whether the candidate separates identity from position. Array indexes are temporary positions, so they must not be React keys or persistent item identities. A stable item ID selects what moves; a final index or neighboring stable ID selects where it moves.

The second signal is accessibility precision. WCAG keyboard operation and the WCAG 2.2 dragging- movement requirement are related but independent. A keyboard-only shortcut does not by itself give a user who can click or tap but cannot hold and drag a single-pointer alternative. Visible Move up, Move down, or Move to controls can satisfy both input paths.

The third signal is interaction-state discipline. A strong answer distinguishes idle, active reorder, commit, and cancel; retains the original order for rollback; handles pointercancel, Escape, and an invalid drop; and does not persist every hover position as a business update.

The fourth signal is assistive-technology behavior. Native list and button semantics, an explicit drag handle, concise instructions, stable focus, a polite status announcement, and visible drop indicators matter more than adding many ARIA attributes. aria-grabbed and aria-dropeffect are deprecated and should not be presented as the solution.

Finally, the interviewer looks for evidence-based verification: pure reorder tests, mouse and touch tests, keyboard-only operation, screen-reader checks, focus assertions, reduced-motion and forced- colors behavior, and save-failure recovery. An automated accessibility scan cannot prove that a spoken reorder flow is understandable.

Questions to Clarify Before Answering

  • Is reordering within one list or between lists? One list needs a final position; cross-list

movement also needs source and destination ownership, allowed drop types, and an atomic update.

  • Must the order be saved remotely? Local-only order can commit immediately. Remote persistence

needs pending, success, failure, retry, and version-conflict behavior.

  • Which inputs are in scope? Mouse-only HTML drag events are insufficient if touch, keyboard,

switch, voice, or screen-reader users are required.

  • Is a click or tap alternative visible? Keyboard shortcuts meet a different need. For a

non-essential sortable list, provide controls that can be activated with one pointer without holding and moving it.

  • Does movement preview immediately or only on drop? Live preview gives stronger visual feedback,

but cancellation must restore the original order and announcements must not chatter on every pointer pixel.

  • Can several items be selected? Multi-item movement changes the model from one item ID to an

ordered set and requires rules for mixed movable and locked items.

  • Is the list virtualized? Off-screen positions do not exist as DOM drop targets, so hit testing,

announcements, total counts, and keyboard movement must operate on the data model.

  • Can another client reorder concurrently? If yes, the save contract needs a list version or

equivalent precondition and an explicit conflict policy.

30-Second Answer Framework

“I would model reorder as one pure command: move a stable item ID to a final index. Pointer drag, keyboard actions, and visible Move buttons all call it. I would use native list and button semantics, preserve focus on the moved handle, and announce its new position. The session stores the original order, so Escape, pointercancel, an invalid drop, or a save failure can restore it. Then I would verify the permutation invariant and test mouse, touch, click-only, keyboard, screen-reader, focus, and failure flows.”

Step-by-Step Deep Dive

Start with one canonical data operation. The input uses a stable ID and a zero-based final index; the output is a new order. The operation must preserve every ID exactly once and leave the input array untouched.

typescript
interface SortableItem {
  id: string
  label: string
}

function moveItem(
  items: ReadonlyArray<SortableItem>,
  itemId: string,
  targetIndex: number,
): ReadonlyArray<SortableItem> {
  const fromIndex = items.findIndex((item) => item.id === itemId)

  if (fromIndex === -1 || items.length < 2) return items

  const finalIndex = Math.max(0, Math.min(targetIndex, items.length - 1))

  if (fromIndex === finalIndex) return items

  const next = [...items]
  const [movedItem] = next.splice(fromIndex, 1)

  if (!movedItem) return items

  next.splice(finalIndex, 0, movedItem)
  return next
}

This command costs O(n) time and O(n) space because array removal, insertion, and copying shift items. That is appropriate for a normal fully rendered task list. A very large virtualized list may store sortable rank keys or apply server-side movement by neighboring IDs, but that added complexity needs evidence from the scale requirement.

Use stable IDs as React keys. With key={item.id}, React can move the existing item node instead of treating a new index as a new identity. This helps preserve the focused handle and local item state. If an asynchronous update still loses focus, keep refs keyed by item ID and restore focus to that same handle after the committed render. Do not send focus to the start of the list, and do not force focus changes for pointer users.

Make the base markup understandable without drag. An ordered list and list items expose collection structure. Each item has a native button named, for example, “Reorder Review.” Provide visible Move up and Move down buttons, or a Move to position action, with the item name in each accessible name. Disable an impossible boundary action. These controls work by click, tap, and keyboard activation, so they are both a discoverable alternative and a simple recovery path when drag fails.

The distinction between two accessibility requirements changes the design. Keyboard operation means the complete reorder can be performed without a pointing device. Separately, dragging movement must have a single-pointer method that does not require holding and moving the pointer when dragging is not essential. Arrow-key drag mode can satisfy keyboard users but not the second requirement unless its controls are also operable by click or tap. Visible movement buttons satisfy both.

An optional compact keyboard drag mode can reduce repeated Tab presses. Focus the drag handle and press Enter or Space to begin; Arrow Up and Arrow Down change the candidate position; Enter or Space commits; Escape cancels and restores the snapshot. Put these instructions in text referenced by the handle so users can discover them. If the item also supports selection, keep selection and reorder commands distinct so Space does not have two meanings on the same focus target.

Represent the interaction as a short-lived session. On start, store itemId, input type, original order, and current candidate index. Pointer or keyboard movement only updates the candidate through moveItem. Commit produces one persistence request and one announcement. Cancel restores the original order and announces cancellation. A new server response should not be allowed to overwrite a newer session merely because its request finished later.

For pointer input, use an explicit handle instead of making the whole row draggable. This prevents a drag from stealing clicks from links, checkboxes, text selection, or scrolling. If implementing in-page sorting with Pointer Events, wait for a small movement threshold, capture the pointer, compute the candidate position from item midpoints, expose a non-color-only insertion indicator, and handle pointerup, pointercancel, lost capture, scrolling, and a drop outside the list. A tested drag library is preferable when touch sensors, auto-scroll, collision detection, nested scroll containers, and assistive-technology behavior are all requirements.

Native HTML drag and drop can be reasonable for desktop or cross-application data transfer, but it does not supply a keyboard or screen-reader reorder workflow. Its event model also requires explicit valid drop targets and suppresses other device input events during a drag. Choosing it does not remove the need for move controls, focus behavior, announcements, or touch verification.

Communicate results through both visuals and semantics. Show the candidate insertion point with shape or border as well as color, keep a visible focus indicator, and respect reduced-motion preferences. Use a persistent polite status region for short messages after deliberate keyboard moves, commits, cancels, and failures. A message such as “Moved Review to position 2 of 5” carries item, result, position, and total. Do not announce every pointer hover, and do not recreate the live region at the same moment as its message.

Avoid obsolete drag semantics. WAI-ARIA 1.2 marks aria-grabbed and aria-dropeffect as deprecated. Native elements, accessible names, descriptive instructions, current state conveyed in text, and tested announcements provide a more reliable contract. ARIA does not add missing keyboard behavior or a missing click alternative.

For remote persistence, send the stable ordered IDs or a move command plus the version that the client edited. Optimistically preview the result, expose saving state without blocking keyboard focus, and commit the announcement only under the chosen product rule. On a network failure, either retain the local pending order with Retry or roll back to the snapshot and announce the rollback. On a version conflict, fetch the current order and ask the user to retry or apply a defined merge; silently overwriting another client's order is not a recovery strategy.

Verification starts with the pure command. Test first, last, adjacent, same-position, unknown-ID, and out-of-range moves. For generated input, assert that the output contains the same IDs with the same counts and only the requested relative movement. Then run an interaction matrix:

  • mouse drag, touch drag, click-only controls, and a drop outside the list;
  • keyboard-only move, commit, boundary action, and Escape cancellation;
  • focus remaining on the moved item's handle after each render and rollback;
  • VoiceOver with Safari and NVDA with Firefox or Chrome announcing item, instructions, new position,

cancellation, and save failure once;

  • 200% zoom, forced colors, high contrast, reduced motion, long labels, scrolling, and RTL layout;
  • delayed success, request reordering, offline failure, retry, and version conflict.

Automated accessibility tools can catch missing names, invalid attributes, and some focusability problems. They cannot judge whether the keyboard model is discoverable, whether the spoken sequence is coherent, or whether a touch-screen reader can complete the task. Those require manual use.

High-Quality Sample Answer

“I would keep item identity independent from array position. The reducer accepts an item ID and a final index, returns a new permutation, and is the only code allowed to change order. Pointer drag, Move up or Move down buttons, and any keyboard drag mode all dispatch that same command.

The semantic baseline would be an ordered list with native buttons. Each row has an explicit reorder handle and visible movement controls whose accessible names include the item label. The movement controls matter because keyboard support and a click-or-tap alternative to dragging are separate requirements. I would not depend on aria-grabbed or aria-dropeffect; both are deprecated.

When a reorder starts, I keep the original order and active item ID. Pointer hit testing or arrow keys update only a candidate position. Commit sends one save and announces, for example, ‘Moved Review to position 2 of 5.’ Escape, pointercancel, an invalid drop, or the chosen save-failure rule restores the snapshot. Stable React keys keep the same handle focused after the DOM order changes.

For pointer sorting I would use a dedicated handle, movement threshold, pointer capture, clear insertion indicator, auto-scroll behavior, and cancellation cleanup. If touch, nested scrolling, and cross-browser collision handling are required, I would choose a tested library only after checking its keyboard and screen-reader behavior; a mouse demo is not enough.

I would unit-test the permutation invariant and then manually complete the task by mouse, touch, keyboard, click-only controls, VoiceOver, and NVDA. I would also test focus, Escape, drop outside, reduced motion, forced colors, delayed saves, stale responses, and version conflicts. That verifies the actual interaction contract; an automated scan alone is insufficient.”

Common Mistakes

  • Using array indexes as keys → reordering changes identity, focus, and item-local state → **Use

stable item IDs for keys and commands.**

  • Making drag the only interaction → users who cannot hold and move a pointer cannot reorder →

Provide visible click or tap controls with equivalent results.

  • Adding arrow keys and claiming all requirements are met → keyboard equivalence alone may still

lack a non-dragging single-pointer method → **Assess keyboard and dragging-movement requirements independently.**

  • Attaching drag listeners to the entire row → links, selection, checkboxes, and scrolling

conflict with reorder → Use an explicit focusable handle.

  • Implementing separate reorder algorithms for each input → mouse, touch, and keyboard produce

different edge cases → Route every input to one stable-ID command.

  • Mutating the array or DOM directly → rendered state and saved state diverge → **Return a new

permutation from the state owner.**

  • Using aria-grabbed and aria-dropeffect as the accessibility plan → the attributes are

deprecated and add no interaction → **Use native controls, instructions, state, focus, and tested announcements.**

  • Announcing every pointer movement → the live region becomes noisy and delayed → **Announce

deliberate keyboard steps and final outcomes, not pointer pixels.**

  • Moving focus to the list start after reorder → users lose their place → **Preserve or restore

focus by stable item ID.**

  • Saving every hover position → network races can apply stale intermediate orders → **Persist one

committed move with a version precondition.**

  • Relying only on an automated audit → it cannot validate spoken instructions or complete input

flows → Test with real keyboard, touch, and screen readers.

Follow-Up Questions and Responses

Follow-up 1: What changes when the list is virtualized?

Do not treat mounted DOM rows as the complete collection. Keep movement, total count, and candidate position in the data model. Keyboard commands can move by logical index even when the destination is off-screen, then scroll the moved item into view without losing its focus target. Pointer movement needs model-aware hit testing and auto-scroll. If the library cannot expose accurate collection semantics or stable focus through virtualization, a non-virtualized accessible mode may be the safer trade-off at the measured scale.

Follow-up 2: How would you move several selected items?

Store an ordered set of stable IDs, preserve their relative order, remove them as one group, and insert the group at one final boundary. The announcement includes the item count and destination. Selection and the drag handle need separate commands, especially when Space already toggles selection. Reject a mixed selection containing locked items or define exactly which items remain; silently moving only part of the selection is ambiguous.

Follow-up 3: What if two clients reorder the same list concurrently?

Attach a list version or ETag to the move request. The server accepts the move only if the version matches. On conflict, fetch the current order, retain the user's intended item and destination as context, and offer a retry or apply a documented merge rule. Last-write-wins is acceptable only when the product explicitly accepts losing another user's ordering decision.

Follow-up 4: Would you use native HTML drag and drop, Pointer Events, or a library?

Native drag and drop is useful for desktop and external data transfer, but it does not provide the full keyboard and assistive-technology workflow. Pointer Events offer control for in-page touch and mouse sorting but require collision detection, capture, auto-scroll, and cleanup. A library is the best choice when those behaviors are already tested, provided its actual keyboard, touch-screen reader, focus, and DOM semantics pass the product's verification matrix. The movement controls and canonical reorder command remain regardless of sensor choice.

Follow-up 5: What if the save fails after the user has continued working?

Tag each committed reorder with a client sequence and the server version it was based on. A late failure may roll back only the state derived from that request; it must not replace a newer confirmed order with an old snapshot. The simplest safe policy is to serialize saves, keep one explicit pending order, and block a second commit while allowing focus and reading. A more responsive queue needs rebasing and conflict tests before it is justified.

Public sources

Related questions