Why Keyboard Accessibility Matters
Many people don't use a mouse:
- Motor disabilities— Users with limited mobility may use keyboards, switches, or sip-and-puff devices
- Blind users— Screen reader users navigate primarily with keyboard
- Power users— Many prefer keyboard shortcuts for speed
- Temporary impairments— Broken arm, carpal tunnel, etc.
- Device limitations— Some devices don't have pointer input
WCAG 2.1.1 (Keyboard):"All functionality of the content is operable through a keyboard interface."
Essential Keyboard Controls
| Key | Action |
|---|---|
| Tab | Move to next interactive element |
| Shift+Tab | Move to previous interactive element |
| Enter | Activate links, buttons, submit forms |
| Space | Activate buttons, check checkboxes, expand dropdowns |
| Arrow Keys | Navigate within components (menus, tabs, radio groups) |
| Escape | Close modals, menus, tooltips |
| Home/End | Jump to first/last item in a list or field |
Use Native HTML Elements
Native HTML elements have built-in keyboard support. Use them instead of custom widgets when possible.
| Element | Built-in Keyboard Behavior |
|---|---|
<a href> |
Focusable, Enter activates |
<button> |
Focusable, Enter and Space activate |
<input>,<textarea> |
Focusable, full text editing support |
<select> |
Focusable, arrow keys navigate options |
<details> |
Focusable, Enter/Space toggles |
<dialog> |
Focus trapping, Escape closes |
Don't: Custom div "button"
<div class="btn" onclick="save()">
Save
</div>
<!-- Not focusable, not keyboard operable -->
Do: Real button
<button type="button" onclick="save()">
Save
</button>
<!-- Focusable, works with Enter/Space -->
Making Custom Widgets Keyboard Accessible
If you must create custom interactive elements, add keyboard support manually.
Step 1: Make It Focusable
<!-- Add tabindex="0" to include in tab order -->
<div role="button" tabindex="0">Custom Button</div>
Step 2: Handle Keyboard Events
element.addEventListener('keydown', (e) =>{
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
activateButton();
}
});
Step 3: Add ARIA Role
<div role="button" tabindex="0" aria-pressed="false">
Toggle Setting
</div>
Prefer Native Elements
This is a lot of work to replicate what<button>gives you for free. Only create custom widgets when absolutely necessary.
Common Keyboard Patterns
Navigation Menus
- Tab— Enter/exit menu
- Arrow keys— Move between items
- Enter— Activate item
- Escape— Close submenu, return to parent
Tab Panels
- Tab— Move to tab list, then into active panel
- Left/Right Arrow— Switch between tabs
- Home/End— First/last tab
<div role="tablist">
<button role="tab" aria-selected="true" aria-controls="panel1">Tab 1</button>
<button role="tab" aria-selected="false" aria-controls="panel2">Tab 2</button>
</div>
<div role="tabpanel" id="panel1">Content 1</div>
<div role="tabpanel" id="panel2" hidden>Content 2</div>
Modal Dialogs
- Tab— Cycles through focusable elements (trapped)
- Escape— Closes modal
- Focus moves into modal on open
- Focus returns to trigger on close
Accordions/Disclosures
- Enter/Space— Toggle open/closed
- Tab— Move between accordion headers
<!-- Native HTML disclosure -->
<details>
<summary>Section Title</summary>
<p>Content that can be expanded/collapsed.</p>
</details>
Skip Links
Allow keyboard users to bypass repetitive navigation:
<!-- First element in body -->
<a href="#maincontent" class="skip-link">Skip to main content</a>
<!-- CSS: visible only on focus -->
<style>
.skip-link {
position: absolute;
top: -40px;
left: 0;
padding: 8px 16px;
background: #000;
color: #fff;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
</style>
<!-- Target element -->
<main id="maincontent">...</main>
Common Keyboard Accessibility Failures
| Problem | Solution |
|---|---|
| Click-only interactions | Add keyboard event handlers (keydown for Enter/Space) |
| Hover-only content | Also show on focus; make content persistent |
| Can't reach element with Tab | Use native interactive element or add tabindex="0" |
| Can't see where focus is | Add visible :focus styles; never use outline:none alone |
| Focus gets trapped | Ensure Escape closes overlays; check for infinite loops |
| Focus gets lost | Manage focus when content changes (modals, deletions) |
| Wrong tab order | Match DOM order to visual order; avoid positive tabindex |
| Can't operate carousel/slider | Add keyboard controls (arrows, buttons) |
Testing Keyboard Accessibility
- Put your mouse away— Literally move it out of reach
- Start at the browser address bar
- Tab through the entire page
- Can you reach every interactive element?
- Can you always see where focus is?
- Does the tab order make sense?
- Test all interactions
- Can you activate every button and link?
- Can you open and close menus, modals, accordions?
- Can you submit forms?
- Check for keyboard traps
- Can you Tab away from every element?
- Does Escape close overlays?
Quick Keyboard Test Script
// Paste in browser console to log focus changes
document.addEventListener('focusin', (e) =>{
console.log('Focus:', e.target);
});