Keyboard Navigation
Keyboard navigation patterns, focus order, and shortcut design for fully keyboard-accessible interfaces
You are an expert in keyboard navigation for building accessible web applications. ## Key Points - Unplug the mouse and navigate the entire application using only the keyboard. - Verify that the focus indicator is always visible on the currently focused element. - Confirm that no keyboard traps exist—the user should always be able to Tab or Escape out of any component. - Check that the focus order matches the visual reading order. - Test that custom widgets follow the expected arrow-key patterns from the WAI-ARIA Authoring Practices. - Use native HTML elements (`<button>`, `<a>`, `<input>`) whenever possible; they come with built-in keyboard behavior. - Never remove the default focus outline (`outline: none`) without providing a custom visible focus indicator. - Follow the WAI-ARIA Authoring Practices Guide for keyboard interaction patterns in custom widgets. - Using `<div onclick>` instead of `<button>`, which is not keyboard-focusable or activatable by default. - Setting `tabindex` to positive values (e.g., `tabindex="5"`), which breaks the natural focus order and is nearly impossible to maintain. ## Quick Example ```html <div role="tablist"> <button role="tab" tabindex="0" aria-selected="true" id="tab-1">Tab 1</button> <button role="tab" tabindex="-1" aria-selected="false" id="tab-2">Tab 2</button> <button role="tab" tabindex="-1" aria-selected="false" id="tab-3">Tab 3</button> </div> ```
skilldb get accessibility-skills/Keyboard NavigationFull skill: 181 linesKeyboard Navigation — Web Accessibility
You are an expert in keyboard navigation for building accessible web applications.
Core Philosophy
Overview
Keyboard accessibility is a foundational requirement of WCAG (Success Criterion 2.1.1). Many users rely exclusively on the keyboard, including people with motor disabilities, power users, and screen reader users. Every interactive element must be reachable and operable without a mouse.
Core Concepts
Default keyboard interactions
| Key | Behavior |
|---|---|
| Tab | Move focus to the next focusable element |
| Shift+Tab | Move focus to the previous focusable element |
| Enter | Activate links, buttons, and form submissions |
| Space | Activate buttons, toggle checkboxes |
| Arrow keys | Navigate within composite widgets (tabs, menus, radio groups) |
| Escape | Close modals, menus, and popups |
| Home/End | Move to first/last item in a list or menu |
Focus order
Focus order must follow a logical reading sequence, typically left-to-right, top-to-bottom for LTR languages. It is determined by DOM order, not CSS visual order. Avoid positive tabindex values, which override the natural order and create confusion.
Focusable elements
Natively focusable: <a href>, <button>, <input>, <select>, <textarea>, elements with tabindex="0".
Not focusable by default: <div>, <span>, <p>. These require tabindex="0" and appropriate ARIA roles to become interactive.
Implementation Patterns
Skip navigation link
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<header><!-- site navigation --></header>
<main id="main-content" tabindex="-1">
<!-- page content -->
</main>
</body>
.skip-link {
position: absolute;
top: -100%;
left: 0;
padding: 0.5rem 1rem;
background: #000;
color: #fff;
z-index: 1000;
}
.skip-link:focus {
top: 0;
}
Roving tabindex for composite widgets
<div role="tablist">
<button role="tab" tabindex="0" aria-selected="true" id="tab-1">Tab 1</button>
<button role="tab" tabindex="-1" aria-selected="false" id="tab-2">Tab 2</button>
<button role="tab" tabindex="-1" aria-selected="false" id="tab-3">Tab 3</button>
</div>
const tabs = document.querySelectorAll('[role="tab"]');
function activateTab(target) {
tabs.forEach(tab => {
tab.setAttribute('tabindex', '-1');
tab.setAttribute('aria-selected', 'false');
});
target.setAttribute('tabindex', '0');
target.setAttribute('aria-selected', 'true');
target.focus();
}
tabs.forEach(tab => {
tab.addEventListener('keydown', (e) => {
const index = Array.from(tabs).indexOf(e.currentTarget);
let newIndex;
switch (e.key) {
case 'ArrowRight':
newIndex = (index + 1) % tabs.length;
break;
case 'ArrowLeft':
newIndex = (index - 1 + tabs.length) % tabs.length;
break;
case 'Home':
newIndex = 0;
break;
case 'End':
newIndex = tabs.length - 1;
break;
default:
return;
}
e.preventDefault();
activateTab(tabs[newIndex]);
});
});
Keyboard-accessible custom dropdown
function handleDropdownKeydown(event, items) {
const currentIndex = items.indexOf(document.activeElement);
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
const nextIndex = Math.min(currentIndex + 1, items.length - 1);
items[nextIndex].focus();
break;
case 'ArrowUp':
event.preventDefault();
const prevIndex = Math.max(currentIndex - 1, 0);
items[prevIndex].focus();
break;
case 'Escape':
closeDropdown();
triggerButton.focus();
break;
case 'Enter':
case ' ':
event.preventDefault();
selectItem(items[currentIndex]);
break;
}
}
Testing & Validation
- Unplug the mouse and navigate the entire application using only the keyboard.
- Verify that the focus indicator is always visible on the currently focused element.
- Confirm that no keyboard traps exist—the user should always be able to Tab or Escape out of any component.
- Check that the focus order matches the visual reading order.
- Test that custom widgets follow the expected arrow-key patterns from the WAI-ARIA Authoring Practices.
Best Practices
- Use native HTML elements (
<button>,<a>,<input>) whenever possible; they come with built-in keyboard behavior. - Never remove the default focus outline (
outline: none) without providing a custom visible focus indicator. - Follow the WAI-ARIA Authoring Practices Guide for keyboard interaction patterns in custom widgets.
Common Pitfalls
- Using
<div onclick>instead of<button>, which is not keyboard-focusable or activatable by default. - Setting
tabindexto positive values (e.g.,tabindex="5"), which breaks the natural focus order and is nearly impossible to maintain.
Anti-Patterns
Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.
Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.
Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.
Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.
Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.
Install this skill directly: skilldb add accessibility-skills
Related Skills
Accessible Forms
Accessible form design patterns including labels, validation, error handling, and multi-step forms
Aria Patterns
ARIA roles, states, and properties for building accessible custom widgets and UI components
Axe Testing
Automated accessibility testing with axe-core, including CI integration, custom rules, and result analysis
Color Contrast
Color contrast ratios, visual accessibility, and inclusive design for users with low vision or color blindness
Focus Management
Focus management strategies for single-page applications, modals, route changes, and dynamic content
Screen Reader Compat
Building web content that works correctly with screen readers like NVDA, JAWS, and VoiceOver