Keyboard Navigation and Focus Management

Many people never touch a mouse. Screen reader users, people with tremors or limited hand movement, switch and voice-control users, and plenty of power users move through interfaces with the keyboard. For all of them, focus — the element that currently receives key presses — is their cursor. If focus is invisible, jumps somewhere unexpected, gets lost when content changes, or cannot reach a control at all, the interface is broken for them, however good it looks.

Keyboard accessibility has two halves. Keyboard navigation means every function can be reached and operated with keys, in a sensible order, using familiar conventions. Focus management means that when the interface changes — a dialog opens, a row is deleted, a single-page app changes route — the code deliberately puts focus somewhere useful instead of leaving users stranded.

TL;DR

Quick Example

A settings dialog built on the native dialog element. showModal() moves focus inside, makes the rest of the page inert, and closes on Escape; the close handler returns focus to the button that opened it.

Compare this with a hand-built modal made of divs: it would need a focus trap, an Escape handler, aria-modal, background hiding, and focus restoration — and each is a chance to get something wrong.

Core Concepts

Focusable Versus Tabbable

An element is focusable if script or a click can focus it, and tabbable if it is also part of the sequential Tab order.

Focus Order

The Tab order follows the DOM. When CSS reorders content visually — flexbox order, grid placement, absolute positioning — keyboard users experience the source order, so focus appears to jump around the screen. WCAG 2.4.3 (Focus Order) requires an order that preserves meaning and operability; the reliable way to meet it is to keep the source order and the visual order the same.

Visible Focus

Users need to see where focus is. WCAG 2.2 requires a visible focus indicator (2.4.7) and that the focused element is not entirely hidden by sticky headers, banners, or other author-created content (2.4.11, Focus Not Obscured). The :focus-visible pseudo-class shows a strong indicator for keyboard users without showing it on every mouse click.

Keyboard Conventions

Users learn these conventions from their operating system and browser. A custom widget that invents its own keys forces every user to discover them.

Keyboard Traps and Intentional Containment

A keyboard trap — focus enters a component and cannot leave — fails WCAG 2.1.2. Modal dialogs are the deliberate exception: focus stays inside while the dialog is open, because the page behind is unavailable, but Escape or a close button always offers a way out.

Skip Links

Every page with repeated navigation should start with a link that jumps straight to the main content (WCAG 2.4.1, Bypass Blocks). It can be visually hidden until focused, so sighted mouse users never see it.

The tabindex="-1" on main makes the jump move keyboard focus reliably in every browser, not only the scroll position.

Composite Widgets and Roving Tabindex

A toolbar with ten buttons, or a tab list with eight tabs, should cost one Tab press, not ten. The roving tabindex pattern keeps exactly one item in the Tab order (tabindex="0") and the rest at tabindex="-1"; arrow keys move focus and the 0 along with it.

The alternative, aria-activedescendant, leaves DOM focus on the container and points to the active item by ID. It suits comboboxes and large listboxes where focus must stay in a text input. The ARIA Authoring Practices Guide documents the expected keys for each widget.

Managing Focus When the Interface Changes

Route Changes in Single-Page Apps

A full page load resets focus and screen readers announce the new title. A client-side route change does neither, so keyboard users stay focused on the link they just activated, now in a page that has been replaced. After the new view renders, move focus to its heading and update the title. Some frameworks announce route changes or reset focus for you — check what yours does before adding your own.

Making the Background Inert

For drawers, off-canvas menus, and custom modal layers, the inert attribute removes a whole subtree from focus, clicks, and the accessibility tree. It replaces fragile focus-trap scripts that try to intercept Tab at the edges.

Remove inert when the layer closes, then restore focus to the trigger.

Best Practices

Reach for Native Elements First

A button handles Enter and Space, appears in the Tab order, and announces itself correctly. details and summary give a disclosure widget, dialog gives a modal, and a native select gives a keyboard-friendly listbox. Build custom widgets only when native ones cannot do the job.

Make Every Pointer Action Available From the Keyboard

Drag and drop, hover-only menus, swipe gestures, and right-click menus need keyboard equivalents — move up and down buttons, menus that open on focus or Enter, and visible controls for hidden actions.

Keep Keyboard Shortcuts Safe

Single-character shortcuts can be triggered accidentally by speech input users. WCAG 2.1.4 requires that they can be turned off, remapped, or apply only while a component has focus. Prefer modifier-key shortcuts and document them.

Test With the Keyboard on Every Change

Put the mouse aside and Tab through new screens and widgets before merging. Encode critical flows as automated keyboard tests — see Accessibility Testing.

Common Mistakes

Removing the Focus Outline

Positive tabindex to "Fix" the Order

Losing Focus When Content Disappears

Clickable div Without Keyboard Support

FAQ

What is the difference between :focus and :focus-visible?

:focus matches whenever an element has focus, however it got there. :focus-visible matches when the browser judges that a focus indicator is helpful — typically keyboard navigation, and always for text inputs. Style :focus-visible with a strong indicator so keyboard users see focus without outlines appearing on every mouse click.

When should I use tabindex="0"?

Only on a custom interactive element that also has an appropriate role, an accessible name, and keyboard handling — for example a custom slider. If a native element can do the job, use it instead. Never add tabindex="0" to non-interactive content such as paragraphs just so screen reader users can "reach" it; they read content with their own navigation commands.

How do I trap focus inside a modal?

Prefer the native dialog element opened with showModal(), which makes the rest of the page inert. For custom layers, set inert on everything outside the modal. Always provide Escape and a visible close button, and return focus to the trigger on close.

What is roving tabindex?

A pattern for composite widgets in which only one item has tabindex="0" and the others tabindex="-1". Tab moves into and out of the widget as a single stop; arrow keys move focus between items and carry the 0 with them, so returning to the widget lands on the last active item.

Where should focus go after a single-page app navigates?

To the new page's main heading (given tabindex="-1") or to the main content region, with the document title updated so screen readers announce the change. Leaving focus on the old link means keyboard users must start again from wherever the browser happens to put them.

Are keyboard requirements covered by automated tests?

Only partly. Scanners can flag positive tabindex, missing names, and some focus-visibility problems, but they cannot judge order or focus movement. Script important flows with an end-to-end tool such as Playwright, asserting which element is focused after each key press.

Related Topics

References