close

DEV Community

babycat
babycat

Posted on

Add Arrow-Key Shortcuts to a Confirmation Dialog Without Breaking Accessibility

Two buttons in a confirmation dialog look simple: Cancel and Confirm. Keyboard behavior makes the component a small state machine.

A recent MonkeyCode change gives us a concrete example. Issue #862 and PR #863 add these shortcuts to the slash-command confirmation:

ArrowLeft  -> focus Cancel
ArrowRight -> focus Confirm
Enter fullscreen mode Exit fullscreen mode

The reviewed implementation at commit c58bcd4 moves focus through button refs. That is a useful extra interaction. It is not a replacement for the dialog's accessibility foundation.

Keep the baseline first

For an alert-style confirmation, users still need:

  • an accessible name and description;
  • focus moved inside when the dialog opens;
  • Tab and Shift+Tab constrained to dialog controls;
  • Escape to dismiss when cancellation is allowed;
  • visible focus;
  • focus returned to the trigger after close;
  • actual buttons whose labels explain the actions.

The WAI-ARIA Authoring Practices Alert Dialog Pattern describes the modal semantics and keyboard foundation. Left/right mapping is a product shortcut, not a required AlertDialog convention. That means we must not steal keys from the established behavior around it.

Isolate the extra mapping

The companion keyboard.mjs starts with a pure function:

export function arrowAction(key) {
  if (key === "ArrowLeft") return "cancel";
  if (key === "ArrowRight") return "confirm";
  return null;
}
Enter fullscreen mode Exit fullscreen mode

The event handler ignores unrelated and modified keys:

export function handleDialogArrow(event, controls) {
  const action = arrowAction(event.key);
  if (!action || event.altKey || event.ctrlKey || event.metaKey) return false;
  event.preventDefault();
  controls[action].focus();
  return true;
}
Enter fullscreen mode Exit fullscreen mode

Notice what is absent: no handler for Tab, Shift+Tab, Escape, or Enter. The native <dialog> and buttons in the minimal demo retain their normal jobs. In a React component, use a well-tested modal/dialog primitive for focus containment and dismissal, then add this narrow handler to its content.

A complete minimal dialog

The included dialog.html uses native semantics:

<button id="open">Run slash command</button>
<dialog id="confirm" aria-labelledby="title" aria-describedby="description">
  <h2 id="title">Run this slash command?</h2>
  <p id="description">The task may change files in the workspace.</p>
  <form method="dialog">
    <button id="cancel" value="cancel">Cancel</button>
    <button id="accept" value="confirm">Confirm</button>
  </form>
</dialog>
Enter fullscreen mode Exit fullscreen mode

On open, the demo focuses Cancel. That conservative default avoids placing initial focus on a potentially destructive action. On close, it restores focus to the trigger and announces the result in an aria-live status.

Test the mapping and the interaction

Run the pure mapping test:

node test-keyboard.mjs
Enter fullscreen mode Exit fullscreen mode

Expected output:

PASS arrows move focus; Tab, Escape, and modified arrows remain untouched
Enter fullscreen mode Exit fullscreen mode

Then serve the directory over HTTP and test dialog.html with keyboard only. The manual checklist is:

  1. Open from the trigger; focus is visible on Cancel.
  2. Press Right; focus moves to Confirm without executing it.
  3. Press Left; focus returns to Cancel.
  4. Use Tab and Shift+Tab; focus stays within the modal.
  5. Press Escape; the dialog closes and focus returns to the trigger.
  6. Reopen, focus Confirm, press Enter; the button activates once.
  7. Zoom to 200% and repeat without losing labels or focus indication.

Also test with a screen reader. Arrow keys can participate in screen-reader navigation modes, so the control must remain understandable without discovering the shortcut. Never make the arrows the only way to choose an action.

The design rule is pleasantly small: add shortcuts as an enhancement around semantic controls, and prove that the keys you did not handle still work.

Disclosure: I'm a MonkeyCode user, not affiliated with the project. The product behavior is based on the linked public issue, PR, and pinned source; the standalone mapping tests were run locally, not as a full MonkeyCode accessibility audit.

Top comments (3)

Collapse
 
a11ysolutions profile image
A11ySolutions

"prove that the keys you did not handle still work", this should be printed on a poster somewhere. It matters for AI agents too: they navigate dialogs through the same semantics. There's a CHI paper this year (A11y-CUA) where agent task success dropped from 78% to 42% keyboard-only. Same baseline, one more reason to get it right.

Collapse
 
frank_signorini profile image
Frank

How do you handle cases where the dialog has multiple focusable elements and the user navigates with arrow keys? I'd love to swap ideas on this.

Collapse
 
odd_background_328 profile image
Odd_Background_328

The keyboard-only result sounds interesting. Do you have a link to the A11y-CUA paper or the exact study setup? I’d like to understand which dialog semantics caused the largest drop before drawing conclusions from the percentage.