Keyboard Navigation
Keyboard accessibility is the backbone of web accessibility. If your website works well with a keyboard, it is far more likely to work with screen readers, switch devices, voice control, and other assistive technologies. Conversely, if keyboard users cannot navigate your site, neither can most assistive technology users. This lesson covers the principles, techniques, and testing strategies for ensuring robust keyboard accessibility.
Why Keyboard Accessibility Matters
Keyboard accessibility is not just for people who cannot use a mouse. It benefits a wide range of users:
- People with motor disabilities who cannot use a mouse or trackpad. They may use a keyboard, a switch device, a sip-and-puff device, or other adaptive hardware — all of which typically map to keyboard events.
- Screen reader users who navigate primarily with keyboard commands. If an element is not keyboard-accessible, it is not screen-reader-accessible either.
- Voice control users who use software like Dragon NaturallySpeaking or Apple Voice Control. These tools often simulate keyboard actions.
- Power users who prefer keyboard shortcuts for efficiency. Developers, data entry professionals, and many others prefer the keyboard for speed.
- Temporary situations — someone with a broken arm, using a trackpad on a bumpy train, or working on a device without a mouse.
Tab Order and Focus Management
When a user presses the Tab key, focus moves to the next interactive element in the tab order. The tab order is determined by the DOM (Document Object Model) order of elements in the HTML source code.
Default Tab Order
By default, these elements are part of the tab order:
<a href="...">(links with an href attribute)<button><input>,<select>,<textarea>(form controls)<details>/<summary>- Any element with
tabindex="0"
Non-interactive elements like <div>, <span>, <p>, and <section> are not in the tab order by default, and that is correct — you should not add tabindex to elements that are not interactive.
The tabindex Attribute
The tabindex attribute controls whether and how an element participates in the tab order:
tabindex="0"— Adds a non-interactive element to the natural tab order. Use this when you are building a custom interactive widget from a<div>(though prefer using native interactive elements when possible).tabindex="-1"— Removes an element from the tab order but allows it to receive programmatic focus via JavaScript (element.focus()). Useful for managing focus in modals and SPAs.tabindex="1"or higher — Avoid this. Positive tabindex values force elements to the front of the tab order, which almost always disrupts the natural reading sequence and confuses users.
tabindex values greater than 0. They create a confusing, unpredictable tab order that is nearly impossible to maintain as the page evolves. If you need an element to come first in the tab order, move it earlier in the DOM.
Visual vs DOM Order
CSS can visually rearrange elements on the page without changing the DOM order. Properties like flex-direction: row-reverse, order, position: absolute, and CSS Grid placement can all create situations where the visual order does not match the DOM order. Keyboard users will tab through elements in DOM order, which can be disorienting if it does not match what they see.
WCAG 1.3.2 (Meaningful Sequence, Level A) requires that the reading order (DOM order) is meaningful. Always ensure that your DOM order matches the intended visual reading order.
Focus Indicators
A focus indicator is the visual cue that shows which element currently has keyboard focus. Without a visible focus indicator, keyboard users cannot tell where they are on the page.
Default Browser Focus Styles
Browsers provide default focus outlines, but they vary in visibility:
- Chrome: A blue outline ring
- Firefox: A dotted outline
- Safari: A blue glow/ring
Many developers remove these defaults with outline: none for aesthetic reasons. This is a critical accessibility violation unless you replace the default with a custom focus style that is equally visible.
Designing Good Focus Indicators
WCAG 2.4.7 (Focus Visible, Level AA) requires that focus is visible. WCAG 2.4.11 (Focus Not Obscured (Minimum), Level AA) in WCAG 2.2 further requires that the focused element is not entirely hidden by author-created content. Here are guidelines for effective focus styles:
/* Remove default and add custom focus style */
:focus {
outline: none;
}
:focus-visible {
outline: 2px solid #005fcc;
outline-offset: 2px;
border-radius: 2px;
}
/* Only show focus styles for keyboard navigation */
/* :focus-visible is supported in all modern browsers */
- Use
:focus-visibleinstead of:focus—:focus-visibleonly applies focus styles when the user is navigating with the keyboard, not when clicking with a mouse. This gives you the best of both worlds: visible focus for keyboard users and no distracting outlines for mouse users. - Ensure the focus indicator has 3:1 contrast against adjacent colors (WCAG 1.4.11 Non-text Contrast).
- Use an outline, not just a color change. A color change alone may not be perceivable by color-blind users. An outline or border change provides a shape-based indicator.
- Make it thick enough to see. A 2px outline is generally the minimum thickness for good visibility.
- Use
outline-offsetto add space between the element and the focus ring, improving visibility on dark or busy backgrounds.
*:focus { outline: none; } in a CSS file, it is a red flag. Either add a :focus-visible replacement or remove the rule entirely.
Skip Links
A skip link (also called a "skip navigation" link) is a link that appears at the very top of the page, typically visible only when focused. It allows keyboard users to skip past repetitive navigation and jump directly to the main content area.
Why Skip Links Matter
Imagine pressing Tab 20 or 30 times through a navigation menu on every single page just to reach the content. For keyboard users, this is exhausting and time-consuming. A skip link lets them bypass the navigation in a single keypress.
Implementation
<!-- Place as the first element inside <body> -->
<a href="#main-content" class="skip-link">Skip to main content</a>
<header>
<nav>...navigation items...</nav>
</header>
<main id="main-content" tabindex="-1">
...page content...
</main>
/* Visually hidden but appears on focus */
.skip-link {
position: absolute;
top: -100%;
left: 0;
padding: 8px 16px;
background: #005fcc;
color: #ffffff;
font-weight: bold;
z-index: 1000;
text-decoration: none;
}
.skip-link:focus {
top: 0;
}
Key implementation details:
- The skip link must be the first focusable element on the page
- The target element (
#main-content) should havetabindex="-1"so it can receive focus programmatically - The skip link should be visually hidden by default but become visible when it receives focus
- The link text should be descriptive: "Skip to main content" is the conventional text
Managing Focus in SPAs and Modals
Single-Page Applications (SPAs)
In traditional multi-page websites, navigating to a new page resets focus to the top of the page. In SPAs (React, Angular, Vue), navigation happens client-side without a full page reload, which means focus management is your responsibility.
When a route changes in an SPA:
- Move focus to the main content area or the page heading. Without this, focus stays on the link that was clicked, and the screen reader user has no indication that the page content has changed.
- Announce the new page by updating the document title and optionally using an ARIA live region to announce the navigation.
- Manage the skip link target. Ensure the skip link still works after client-side navigation.
// Example: Focus management after SPA route change
function onRouteChange() {
// Update the page title
document.title = 'New Page Title — My App';
// Move focus to the main content heading
const heading = document.querySelector('main h1');
if (heading) {
heading.setAttribute('tabindex', '-1');
heading.focus();
}
}
Modal Dialogs
Modals are one of the most complex keyboard accessibility patterns. When done correctly, they provide a contained, focused experience. When done incorrectly, they trap users or let focus leak to the background page.
The correct modal focus management pattern:
- When the modal opens:
- Move focus to the first focusable element in the modal (or to the modal container itself)
- Trap focus within the modal — Tab and Shift+Tab should cycle through elements inside the modal only
- Add
aria-hidden="true"to the main page content behind the modal (or use the native<dialog>element, which handles this automatically)
- While the modal is open:
- Focus must not escape to elements behind the modal
Escapekey should close the modal
- When the modal closes:
- Return focus to the element that originally opened the modal
- Remove
aria-hidden="true"from the main page content
<!-- Use the native dialog element for built-in accessibility -->
<dialog id="my-modal">
<h2>Modal Title</h2>
<p>Modal content...</p>
<button onclick="this.closest('dialog').close()">Close</button>
</dialog>
<button onclick="document.getElementById('my-modal').showModal()">
Open Modal
</button>
<dialog> element (with the .showModal() method) handles focus trapping, Escape key closing, and background inertia automatically. Use it whenever possible instead of building custom modal behavior from scratch.
ARIA Roles for Custom Interactive Elements
When you build custom interactive components using non-semantic elements (like <div> or <span>), you must add the appropriate ARIA roles, states, and keyboard interactions to make them accessible. Here are the most common patterns:
Custom Buttons
<!-- Prefer native buttons -->
<button>Click me</button>
<!-- If you must use a div, add role, tabindex, and keyboard handler -->
<div role="button" tabindex="0"
onkeydown="if(event.key==='Enter'||event.key===' '){this.click()}">
Click me
</div>
Tab Panels
<div role="tablist" aria-label="Settings">
<button role="tab" aria-selected="true"
aria-controls="panel-general" id="tab-general">
General
</button>
<button role="tab" aria-selected="false"
aria-controls="panel-security" id="tab-security"
tabindex="-1">
Security
</button>
</div>
<div role="tabpanel" id="panel-general"
aria-labelledby="tab-general">
General settings content...
</div>
<div role="tabpanel" id="panel-security"
aria-labelledby="tab-security" hidden>
Security settings content...
</div>
Keyboard interactions for tabs: Arrow keys move between tabs, Tab moves into the panel content, and the active tab has aria-selected="true" while inactive tabs have tabindex="-1".
Accordions
<div class="accordion">
<h3>
<button aria-expanded="false" aria-controls="section1">
Section 1
</button>
</h3>
<div id="section1" role="region" aria-labelledby="..."
hidden>
Section 1 content...
</div>
</div>
Key behaviors: Enter or Space toggles the section, and aria-expanded communicates the state to screen readers.
Testing Keyboard Accessibility
Here is a systematic approach to keyboard testing:
- Tab through every interactive element. Starting from the browser address bar, press Tab repeatedly through the entire page. Every link, button, form control, and custom widget should receive focus.
- Verify focus is visible. At every stop, you should see clearly which element has focus. If focus disappears at any point, there is either a missing focus style or focus is on a hidden element.
- Verify no keyboard traps. You should be able to Tab forward and Shift+Tab backward through every element without getting stuck. Pay special attention to embedded content (iframes, video players) and custom widgets.
- Test all interactions. Activate buttons with Enter and Space. Open dropdown menus. Fill out forms. Submit forms. Open and close modals. Navigate through tab panels with arrow keys.
- Test the skip link. Load the page, press Tab once. A "Skip to main content" link should appear. Press Enter. Focus should jump to the main content area.
- Test modal focus management. Open a modal, verify focus moves into it. Tab through the modal — focus should stay inside. Press Escape — the modal should close and focus should return to the trigger element.
- Test in multiple browsers. Keyboard behavior can differ between browsers. Test at least in Chrome and Firefox.
Resources
- ARIA Authoring Practices Guide (APG) — Official W3C patterns for accessible custom widgets
- WebAIM Keyboard Accessibility Guide — Comprehensive guide to keyboard testing
- MDN: dialog Element — Documentation for the native HTML dialog element