Best Practices

AI Rules for Keyboard Navigation

AI builds mouse-only interfaces where keyboard users cannot reach controls. Rules for tab order, focus management, roving tabindex, skip links, keyboard shortcuts, and focus-visible styling.

7 min read·April 7, 2025

16 Tab presses to reach main content, no focus indicator, modals that trap you in the background

Tab order, focus management, roving tabindex, skip links, :focus-visible styling

AI Builds Interfaces That Require a Mouse

AI generates interfaces with: custom interactive elements that are not focusable (div-based buttons, span-based links — not in the tab order), no visible focus indicator (outline: none on everything — keyboard users cannot see where they are), illogical tab order (focus jumps from header to footer to sidebar — does not follow visual flow), no focus management (opening a modal does not move focus to it, closing does not return focus to the trigger), and no skip navigation (keyboard users must tab through 50 navigation links to reach the main content). 15% of users rely on keyboard navigation: motor disabilities, power users, screen reader users, and anyone with a broken mouse.

Modern keyboard navigation is: logically ordered (tab order follows visual reading flow — left to right, top to bottom), focus-managed (modals trap focus, SPA navigation moves focus to new content), roving-indexed (composite widgets like tabs and menus use arrow keys internally), skip-equipped (skip-to-content link as the first focusable element), and visibly focused (:focus-visible provides a clear indicator for keyboard users without affecting mouse users). AI generates none of these.

These rules cover: logical tab order, focus management for modals and SPAs, roving tabindex for composite widgets, skip-to-content links, keyboard shortcut design, and :focus-visible styling.

Rule 1: Logical Tab Order

The rule: 'Tab order must follow the visual reading flow. The default tab order follows DOM order — which should match the visual layout. Do not use positive tabIndex values (tabIndex="5") to reorder — they create unpredictable tab sequences that break when elements are added or removed. Use tabIndex="0" to add non-native elements to the tab order (only when a native element cannot be used). Use tabIndex="-1" to make elements programmatically focusable but not in the tab order (for focus management targets).'

For DOM order alignment: 'If the visual layout shows: header, then sidebar, then main content — the DOM should be: header, sidebar, main content. CSS can visually reposition elements (grid order, flexbox order, absolute positioning) but the tab order follows the DOM, not the visual layout. If CSS reordering causes the tab order to differ from the visual order: restructure the DOM to match. Test: press Tab repeatedly and verify focus moves through the page in the order a user would expect by reading left-to-right, top-to-bottom.'

AI generates: tabIndex="3" on a sidebar button, tabIndex="1" on a header link, tabIndex="2" on a form input — manual tab ordering that breaks when any element is added. Or: elements visually in one order but in a different DOM order (focus jumps unpredictably). Logical tab order: DOM matches visual flow, no positive tabIndex values, and a Tab-through test catches order issues in seconds.

  • Tab order follows DOM order — DOM must match visual reading flow
  • Never positive tabIndex values (1, 2, 3) — unpredictable, fragile, breaks on any DOM change
  • tabIndex='0': add to tab order (non-native elements only when necessary)
  • tabIndex='-1': programmatically focusable but not in tab order (focus management targets)
  • CSS reordering does not change tab order — restructure DOM if visual and tab order diverge

Rule 2: Focus Management for Modals and SPAs

The rule: 'When UI context changes, manage focus explicitly. Modal opens: move focus to the first focusable element inside the modal (or the modal container with tabIndex="-1"). Modal closes: return focus to the element that triggered the modal. SPA route change: move focus to the main content heading (the <h1> of the new page). Toast notification: do not steal focus (use aria-live instead). Dropdown opens: move focus to the first option. Dropdown closes: return focus to the trigger button.'

For focus trapping in modals: 'When a modal is open, Tab must cycle within the modal — not escape to the page behind it. Implementation: listen for Tab keydown, if focus is on the last focusable element in the modal and Tab is pressed, move focus to the first focusable element (wrap around). If Shift+Tab is pressed on the first element, move to the last. The inert attribute on the background content is the modern solution: <main inert> makes all background content non-focusable and non-interactive while the modal is open.'

AI generates: a modal that opens with no focus change (focus stays on the trigger behind the modal), Tab escapes the modal to the background page (the user is navigating the invisible background), and closing the modal leaves focus at the end of the document (the user must Tab back to where they were). Three focus management failures in one modal. Managed focus: open moves to modal, Tab is trapped, close returns to trigger. Three fixes, seamless modal experience for keyboard users.

💡 Three Fixes, Seamless Modal

AI modal: focus stays on trigger behind the modal, Tab escapes to background, close leaves focus at document end. Three failures. Managed: open moves focus to modal, Tab is trapped inside, close returns to trigger. Three fixes make the modal seamless for keyboard users.

Rule 3: Roving Tabindex for Composite Widgets

The rule: 'Composite widgets (tabs, menus, toolbars, radio groups) use roving tabindex: only one item in the group is in the tab order (tabIndex="0"), all others are tabIndex="-1". Arrow keys move focus between items (updating which item has tabIndex="0"). Tab moves focus OUT of the group to the next component. This pattern: one Tab stop for the entire group (not 10 Tab stops for 10 tabs), arrow keys for internal navigation (familiar from native UI), and Home/End for first/last item.'

For a tab widget: 'Tab list has 5 tabs. Only the active tab has tabIndex="0". The other 4 have tabIndex="-1". User Tabs into the tab list: focus lands on the active tab. Arrow right: focus moves to the next tab (tabIndex swaps). Arrow left: focus moves to the previous tab. Home: focus moves to the first tab. End: focus moves to the last tab. Tab (not arrow): focus leaves the tab list and moves to the tab panel content. This matches WAI-ARIA Authoring Practices for the tab pattern.'

AI generates: 5 tabs, all with tabIndex="0". The user must Tab 5 times to get through the tab list to the content. Or: all tabs unfocusable (tabIndex not set, cannot reach tabs by keyboard). Roving tabindex: one Tab stop for the group, arrow keys inside, Tab to leave. The keyboard experience matches how native desktop tab widgets work — familiar and efficient.

  • One Tab stop per composite widget — arrow keys for internal navigation
  • Active item: tabIndex='0'. Inactive items: tabIndex='-1'
  • Arrow keys move focus and swap tabIndex values within the group
  • Home/End: jump to first/last item in the group
  • Tab: exit the group to the next component — not move within the group
⚠️ One Tab Stop, Not Ten

5 tabs, all tabIndex='0': user must Tab 5 times to get through the tab list. Roving tabindex: one Tab stop for the group, arrow keys to navigate tabs, Tab to exit to the content. Keyboard experience matches native desktop widgets — familiar and efficient.

Rule 5: :focus-visible Styling

The rule: 'Use :focus-visible instead of :focus for focus indicators. :focus fires on every focus event (keyboard Tab AND mouse click). :focus-visible fires only on keyboard focus (Tab, not click). This means: keyboard users see the focus ring (they need it for navigation), mouse users do not see the focus ring (they do not need it — they can see where they clicked). Style: :focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }. Never: outline: none or *:focus { outline: 0 } — these remove the focus indicator for keyboard users.'

For custom focus indicators: 'The default browser focus ring is functional but may clash with your design. Custom: :focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px; border-radius: 4px; }. Requirements: (1) 3:1 contrast ratio against adjacent colors (WCAG 2.2). (2) At least 2px thick (visible at any size). (3) Offset from the element edge (outline-offset: 2px prevents overlap with element borders). (4) Consistent across all interactive elements (same style for buttons, links, inputs — user recognizes the indicator pattern).'

AI generates: *:focus { outline: none; } or button:focus { outline: 0; box-shadow: none; } — focus indicator removed entirely. Keyboard users: cannot see where they are on the page. They Tab blindly, hoping they are on the right element. With :focus-visible: keyboard users see a clear blue ring on the focused element. Mouse users see no ring (clean visual). Both user groups get the experience they need. One CSS selector serves both.

Complete Keyboard Navigation Rules Template

Consolidated rules for keyboard navigation.

  • Tab order follows visual flow: DOM order matches reading order, no positive tabIndex values
  • Focus management: modal opens → focus to modal, closes → focus to trigger, SPA → focus to h1
  • Focus trap in modals: Tab wraps within modal, inert on background content
  • Roving tabindex: one Tab stop per widget group, arrow keys inside, Tab to exit
  • Skip-to-content link: first focusable element, sr-only until focused, targets #main
  • :focus-visible not :focus: keyboard users see the ring, mouse users do not
  • Custom focus indicator: 2px+ thick, 3:1 contrast, 2px offset, consistent across all elements
  • Never outline: none globally — removing focus indicators makes keyboard navigation impossible