← Back to Web app developers

Skill · Version 1.0.0

Keyboard and focus behavior

Designs predictable keyboard navigation and visible focus across widgets, overlays, deletion and route changes.

keyboard-focus-engineering

Team members using this skill

See how it helps

When is it useful?

Use when mouse-free navigation gets trapped or loses its current position.

What does it help you do?

  • Keeps a logical tab order and avoids positive tabindex workarounds.
  • Chooses the appropriate focus model for composite widgets.
  • Restores meaningful focus after closing, deleting or navigating.

Try asking this

Fix a table-row action dialog so closing returns focus correctly, including when its row was deleted. Delete the originating row while its dialog is open. On closing, place focus on a meaningful remaining table control instead of the removed button.

Detailed documentation

Read the skill’s original instructions, examples and checks in their authored language.

Details…Show less

Keyboard & Focus Engineering

Every interactive user journey must work end-to-end with a keyboard alone, with a clearly visible focus indicator, predictable tab order and deliberate focus restoration when overlays open, close or remove elements (WCAG 2.1.1, 2.1.2, 2.4.1, 2.4.3, 2.4.7, 2.4.11).

1. Tab Order & Tabindex Discipline

  • DOM order drives focus order: arrange elements in the DOM so Tab and Shift+Tab follow the visual reading and task sequence (WCAG 2.4.3). Avoid CSS reordering (flex-direction: row-reverse, arbitrary order or absolute positioning) that disconnects visual order from DOM tab order.
  • Tabindex rules:
    • Prefer native focusable elements (<button>, <a href>, <input>, <select>, <textarea>, <summary>).
    • Use tabindex="0" only to add a custom widget's active entry point to the tab sequence or make a scrollable region keyboard-scrollable.
    • Use tabindex="-1" for elements targeted by programmatic .focus() (headings, dialog containers or inactive items inside a roving-tabindex composite widget).
    • Never use positive tabindex (tabindex="1", 2, etc.) — it hijacks page-wide tab order.
  • Bypass blocks (WCAG 2.4.1): provide a working "Skip to main content" link as the first focusable item on shells with repeated navigation.

2. Composite Widgets: Roving Tabindex vs. aria-activedescendant

Complex widgets (tablists, menus, toolbars, radio groups, tree views, grids) must be a single tab stop:

  • Tab / Shift+Tab moves into and out of the composite widget as a whole; Arrow keys, Home and End move between items inside it.
  • Roving tabindex pattern: give the active item tabindex="0" and all sibling items tabindex="-1". On arrow key press, move tabindex="0" to the target item, set the previous item to -1, and call .focus().
  • aria-activedescendant pattern (for comboboxes where focus stays on the text <input>): keep DOM focus on the <input>, update aria-activedescendant="option-id" on ArrowDown/ArrowUp, and scroll the active option into view (scrollIntoView({ block: 'nearest' })).

3. Focus Lifecycle in Overlays, Deletions & Route Changes

Plan where focus goes before, during and after every structural UI transition:

  • Opening a modal dialog or drawer:
    1. Save document.activeElement before opening.
    2. Move initial focus inside the dialog (first primary field, primary action, or dialog heading/container with tabindex="-1").
    3. Contain Tab/Shift+Tab inside the modal (<dialog>.showModal(), inert on background siblings, or focus trap) and close on Escape.
  • Closing a dialog or popover: restore focus to the opener, or to the nearest logical parent/trigger if the opener was unmounted.
  • Deleting an item from a list or table: never let focus drop to <body>. Move focus to the next sibling item, the previous item if the last one was deleted, or the list heading / "Add" button when empty.
  • Hidden and inert content: ensure elements inside closed drawers, inactive tabs or collapsed panels are unfocusable (hidden, inert or display: none). Never place a focusable element inside an aria-hidden="true" container.

4. Visible Focus Indicators (WCAG 2.4.7 & 2.4.11)

  • Use :focus-visible to render a high-contrast focus ring (at least 2px solid outline with outline-offset and >= 3:1 contrast).
  • Never use outline: none without a visible :focus-visible replacement that survives forced-colors: active mode.
  • Prevent sticky headers or footers from obscuring focused elements (scroll-margin-top / scroll-margin-bottom).

Included files 1

  • SKILL.md