---
title: "Keyboard Navigation and Focus"
slug: "keyboard-accessibility"
category: "frontend"
tags: ["frontend", "accessibility", "wcag", "keyboard", "focus"]
status: "stable"
last_updated: 2026-09-27
summary: "Keyboard operability and focus rules for WCAG 2.2 AA: key bindings, no traps, DOM order, :focus-visible rings, skip links, and focus not obscured."
related: ["[[frontend/accessibility]]", "[[frontend/html-aria-patterns]]", "[[frontend/html]]", "[[frontend/css]]", "[[cheatsheets/aria-attributes]]", "[[howto/run-pa11y-locally]]"]
---

## Overview

Every control a mouse user can reach, a keyboard user must reach too, and they must always see where they are. This page covers the keyboard and focus criteria from the [[frontend/accessibility]] checklist: SC 2.1.1 (Keyboard), SC 2.1.2 (No Keyboard Trap), SC 2.4.1 (Bypass Blocks), SC 2.4.7 (Focus Visible), and SC 2.4.11 (Focus Not Obscured, Minimum). Automated scanners flag few of these reliably, so test them by hand with the Tab key.

## Make every control reachable and operable by keyboard; never trap focus

SC 2.1.1 requires every interactive control to be reachable and operable by keyboard alone. SC 2.1.2 bans trapping focus outside a modal.

- Tab and Shift+Tab must reach every control in DOM order.
- Enter and Space must activate buttons; Enter must follow links.
- Arrow keys must navigate inside composite widgets (menus, tabs, sliders).
- Escape must close modals and dismiss menus, returning focus to the trigger.
- Inside a modal, trap focus within the dialog. Outside a modal, never trap.

Native `<button>`, `<a href>`, and form elements get this behavior for free. Custom widgets must implement it themselves; see [[frontend/html-aria-patterns]] for the dialog, tabs, and listbox key contracts.

## Keep DOM order equal to visual order

Tab order follows the DOM, not the layout. Do not reorder with CSS `order`, `flex-direction: row-reverse`, or `position: absolute` and expect tab order to follow; a keyboard user sees focus jump around the screen.

Avoid positive `tabindex` values. They pull elements ahead of the natural order and are hard to maintain. Use `tabindex="0"` only to make a custom widget focusable, and `tabindex="-1"` for elements you focus from script, such as a dialog heading or an error summary.

See [[frontend/html]] for element-level keyboard semantics and [[frontend/css]] for layout rules that preserve reading order.

## Show a visible focus indicator on every interactive element

SC 2.4.7 requires a visible focus indicator on every interactive element. SC 1.4.11 requires the ring to reach 3:1 contrast against adjacent colors.

Style focus with `:focus-visible`, not `:focus`. The ring appears for keyboard users; pointer clicks stay clean.

```css
:focus-visible {
  outline: 3px solid #005fcc;
  outline-offset: 2px;
}
```

Never ship `outline: none` without a replacement style. Removing the default ring is one of the most common keyboard failures.

## Keep focused elements out from under sticky content

SC 2.4.11 (new in WCAG 2.2) requires that the focused component is not fully hidden by author-created content such as sticky headers, cookie bars, or chat launchers.

Reserve space for sticky headers with `scroll-padding-top` on the scroll container so focused elements scroll into view below them:

```css
html {
  scroll-padding-top: 4rem; /* height of the sticky header */
}
```

Dismissible banners should not cover the bottom of the viewport indefinitely; close them or move them out of the flow once the user interacts.

## Add a skip link as the first focusable element

SC 2.4.1 requires a way to bypass repeated blocks such as navigation. A skip link lets keyboard users jump straight to `<main>`:

```html
<a class="skip-link" href="#main">Skip to content</a>
<!-- ... header and nav ... -->
<main id="main">...</main>
```

Reveal it on focus and hide it off-screen otherwise. See [[frontend/css]] for the off-screen clip pattern. Landmark elements (`<nav>`, `<main>`) also help screen reader users skip, but sighted keyboard users need the visible link.

## Related

- [[frontend/accessibility]]
- [[frontend/html-aria-patterns]]
- [[frontend/html]]
- [[frontend/css]]
- [[cheatsheets/aria-attributes]]
- [[howto/run-pa11y-locally]]
