Keyboard Accessibility Guide: Complete WCAG Compliance for Developers
Keyboard accessibility is foundational to web accessibility—if users can't navigate and operate your site with a keyboard alone, it fails WCAG compliance and excludes millions of users. This includes people with motor disabilities who can't use a mouse, blind users navigating with screen readers, and power users who prefer keyboard efficiency.
WCAG 2.1 AA requires all functionality to be operable via keyboard (Success Criterion 2.1.1) with visible focus indicators (2.4.7) and no keyboard traps (2.1.2). Meeting these requirements isn't difficult technically—but it's frequently overlooked as developers rely on mouse interactions.
Q: What percentage of web accessibility issues relate to keyboard access?
A: Keyboard accessibility failures represent approximately 15-20% of WCAG violations on typical websites. Combined with related focus management issues, keyboard problems affect a majority of accessibility-dependent users.
Why Keyboard Accessibility Matters
Who Relies on Keyboard Navigation
Screen reader users: All screen reader operation happens through keyboard commands. If your site isn't keyboard accessible, it's not screen reader accessible.
Motor disability users: People with conditions affecting fine motor control (Parkinson's, cerebral palsy, RSI) may use keyboards, switch devices, or voice control—all of which map to keyboard events.
Temporary impairments: Broken arm? Carpal tunnel flare-up? Users temporarily unable to use a mouse need keyboard access.
Power users: Many users prefer keyboard shortcuts for efficiency—breaking keyboard access alienates these users too.
Legal Requirements
WCAG 2.1 AA keyboard requirements:
2.1.1 Keyboard (Level A): All functionality available via keyboard
2.1.2 No Keyboard Trap (Level A): Users can navigate away from any component using keyboard
2.1.4 Character Key Shortcuts (Level A): Single-character shortcuts can be disabled/remapped
2.4.3 Focus Order (Level A): Focus moves in logical, predictable sequence
2.4.7 Focus Visible (Level AA): Keyboard focus indicator is visible
Focus Fundamentals
What Gets Focus
Only interactive elements receive keyboard focus by default:
Natively focusable:
- Links (
<a href>) - Buttons (
<button>) - Form inputs (
<input>,<select>,<textarea>) - Interactive elements with
tabindex
Not focusable by default:
- Divs, spans, paragraphs
- Images (unless linked)
- Headings
- Static text
Making Elements Focusable
<!-- Natively focusable - correct -->
<button onclick="doSomething()">Click Me</button>
<!-- Not focusable - wrong -->
<div onclick="doSomething()">Click Me</div>
<!-- Made focusable - acceptable but not ideal -->
<div tabindex="0"
role="button"
onclick="doSomething()"
onkeydown="handleKeydown(event)">
Click Me
</div>Best practice: Use native HTML elements whenever possible. A <button> handles keyboard events automatically; a <div> requires manual implementation.
The tabindex Attribute
tabindex="0": Element is focusable in natural tab order
tabindex="-1": Element is programmatically focusable but not in tab order (useful for focus management)
tabindex="1+" (positive values): Avoid these. They override natural tab order and cause confusion.
<!-- Good: Elements in natural order -->
<button tabindex="0">First</button>
<button tabindex="0">Second</button>
<!-- Bad: Forced ordering creates confusion -->
<button tabindex="2">This is second</button>
<button tabindex="1">This is first despite DOM order</button>Focus Visibility
WCAG 2.4.7 Requirements
Users must be able to see where focus is. The default browser outline satisfies this—unless you remove it.
Never do this without replacement:
/* Accessibility failure */
:focus {
outline: none;
}
* {
outline: 0;
}WCAG 2.2 Enhanced Requirements
WCAG 2.2's Focus Appearance (2.4.13) requires:
- Focus indicator at least 2px thick perimeter
- 3:1 contrast between focused and unfocused states
Implementing Visible Focus
/* Basic compliant focus indicator */
:focus {
outline: 3px solid #005fcc;
outline-offset: 2px;
}
/* Enhanced for better visibility */
:focus {
outline: 3px solid #005fcc;
outline-offset: 2px;
box-shadow: 0 0 0 6px rgba(0, 95, 204, 0.25);
}
/* Use :focus-visible to show only for keyboard */
:focus:not(:focus-visible) {
outline: none;
}
:focus-visible {
outline: 3px solid #005fcc;
outline-offset: 2px;
}Focus Visibility on Different Backgrounds
Ensure focus indicators work on all backgrounds:
/* Light background sections */
.light-section :focus-visible {
outline-color: #005fcc;
}
/* Dark background sections */
.dark-section :focus-visible {
outline-color: #ffffff;
box-shadow: 0 0 0 3px #005fcc;
}
/* Image/complex backgrounds */
.hero :focus-visible {
outline: 3px solid #ffffff;
box-shadow: 0 0 0 6px #000000;
}Tab Order
Natural Tab Order
Tab order should follow logical reading order—typically left-to-right, top-to-bottom in Western languages. This happens automatically when:
- Interactive elements are in logical DOM order
- CSS doesn't visually reorder content
- No positive tabindex values override order
Common Tab Order Problems
CSS reordering without DOM change:
/* Visual order: B, A, C */
/* Tab order: A, B, C - confusing */
.container {
display: flex;
flex-direction: row-reverse;
}Solution: Match DOM order to visual order, or use CSS that doesn't affect order:
<!-- DOM matches visual order -->
<div class="container">
<button>B</button>
<button>A</button>
<button>C</button>
</div>Positive tabindex creating chaos:
<!-- Don't do this -->
<button tabindex="3">Third visually, first in tab order</button>
<button tabindex="1">First visually, second in tab order</button>
<button tabindex="2">Second visually, third in tab order</button>Solution: Remove positive tabindex values; let natural order prevail.
Testing Tab Order
- Click at the start of your page
- Press Tab repeatedly
- Verify:
- Focus moves in logical order - No interactive elements are skipped - Focus indicator is always visible - Order matches visual layout expectations
Keyboard Traps
What Constitutes a Trap
A keyboard trap occurs when users can Tab into an element but can't Tab out. Common causes:
Modal dialogs without escape:
// Trap: focus loops within modal, no exit
modal.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
// Keep focus in modal
e.preventDefault();
focusableElements[nextIndex].focus();
}
// Missing: Escape key handler to close modal
});Infinite scroll areas:
<!-- Trap: Tab never reaches footer -->
<div class="infinite-content">
<!-- Endless focusable items -->
</div>
<footer>
<!-- Never reached via keyboard -->
</footer>Embedded applications:
<!-- Trap: Third-party widget captures focus -->
<iframe src="video-player.html">
<!-- No way to Tab out -->
</iframe>Preventing Keyboard Traps
Modals: Always provide Escape key exit
modal.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeModal();
returnFocusToTrigger();
}
});Infinite scroll: Provide "Skip to footer" or pagination alternative
Embedded content: Test third-party widgets for keyboard accessibility before integration
Focus Trapping (The Good Kind)
For modal dialogs, you want to trap focus within the modal—but provide Escape exit:
function trapFocus(modal) {
const focusableElements = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
modal.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
if (e.key === 'Escape') {
closeModal();
}
});
}Focus Management
When to Manage Focus
Opening modals: Move focus to modal (usually the close button or first interactive element)
Closing modals: Return focus to trigger element
Single-page app navigation: Move focus to new content area or heading
Form validation: Move focus to first error
Delete operations: Move focus to logical next element
Implementation Patterns
Modal opening:
function openModal(modal, trigger) {
modal.hidden = false;
modal.setAttribute('aria-modal', 'true');
// Store trigger for focus return
modal.dataset.trigger = trigger.id;
// Move focus to first focusable element
const firstFocusable = modal.querySelector(
'button, [href], input:not([type="hidden"])'
);
firstFocusable?.focus();
}Modal closing:
function closeModal(modal) {
modal.hidden = true;
modal.setAttribute('aria-modal', 'false');
// Return focus to trigger
const trigger = document.getElementById(modal.dataset.trigger);
trigger?.focus();
}SPA navigation:
function navigateToPage(pageContent) {
// Update content
mainContent.innerHTML = pageContent;
// Move focus to main content heading
const heading = mainContent.querySelector('h1');
heading.tabIndex = -1; // Make focusable
heading.focus();
}Form validation:
function handleFormErrors(errors) {
// Display error messages
displayErrors(errors);
// Move focus to first error
const firstErrorField = document.getElementById(errors[0].fieldId);
firstErrorField?.focus();
// Announce to screen readers
announceToScreenReader(`${errors.length} errors found`);
}Interactive Widget Patterns
Custom Buttons
If you must use non-button elements:
<div role="button"
tabindex="0"
onclick="handleClick()"
onkeydown="handleKeydown(event)"
aria-pressed="false">
Toggle Setting
</div>
<script>
function handleKeydown(event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleClick();
}
}
function handleClick() {
const button = event.currentTarget;
const pressed = button.getAttribute('aria-pressed') === 'true';
button.setAttribute('aria-pressed', !pressed);
}
</script>Better approach: Use <button>:
<button type="button"
aria-pressed="false"
onclick="handleClick()">
Toggle Setting
</button>Dropdown Menus
<div class="dropdown">
<button aria-expanded="false"
aria-haspopup="true"
aria-controls="menu1">
Menu
</button>
<ul id="menu1"
role="menu"
hidden>
<li role="menuitem" tabindex="-1">Option 1</li>
<li role="menuitem" tabindex="-1">Option 2</li>
<li role="menuitem" tabindex="-1">Option 3</li>
</ul>
</div>
<script>
// Arrow keys navigate menu items
// Enter/Space select
// Escape closes
// Tab moves out of menu
</script>Tab Panels
<div class="tabs">
<div role="tablist">
<button role="tab"
aria-selected="true"
aria-controls="panel1"
id="tab1">
Tab 1
</button>
<button role="tab"
aria-selected="false"
aria-controls="panel2"
id="tab2"
tabindex="-1">
Tab 2
</button>
</div>
<div role="tabpanel"
id="panel1"
aria-labelledby="tab1">
Panel 1 content
</div>
<div role="tabpanel"
id="panel2"
aria-labelledby="tab2"
hidden>
Panel 2 content
</div>
</div>
<script>
// Arrow keys move between tabs
// Only active tab is in tab order
// Activating tab shows associated panel
</script>Testing Keyboard Accessibility
Manual Testing Steps
- Unplug your mouse (or disable trackpad)
- Navigate the entire site using only keyboard
- Check every interactive element:
- Can you reach it with Tab? - Can you activate it with Enter/Space? - Can you navigate away? - Is focus visible?
- Test all functionality:
- Forms submit correctly - Menus open and close - Modals trap focus appropriately - Dialogs return focus when closed
Automated Detection
TestParty's scanning detects many keyboard accessibility issues:
- Missing focus indicators
- Elements lacking keyboard operability
- Focus order problems
- Missing ARIA keyboard patterns
For e-commerce sites: TestParty provides implementable fixes for common keyboard failures in checkout flows, product filtering, and navigation patterns.
Recording Issues
Document keyboard issues precisely:
Issue: Add to cart button not keyboard accessible
Element: div.add-to-cart-btn
Steps: Tab to product card, attempt to activate add to cart
Expected: Enter/Space adds product to cart
Actual: No keyboard response; must click with mouse
WCAG: 2.1.1 Keyboard
Fix: Replace div with button element or add keyboard event handlersFAQ Section
Q: Why doesn't my custom dropdown work with keyboard?
A: Custom dropdowns need explicit keyboard handling. Arrow keys should navigate options; Enter/Space should select; Escape should close. Native <select> elements handle this automatically.
Q: Is it okay to remove focus outlines for design reasons?
A: You can customize focus indicators but not remove them entirely. Use :focus-visible to show focus only for keyboard users, not mouse clicks. Always provide visible indication.
Q: How do I handle keyboard in single-page applications?
A: Manage focus on route changes—move focus to the main heading or content area. Announce navigation to screen readers. Maintain logical tab order after dynamic content updates.
Q: What about keyboard shortcuts?
A: Custom keyboard shortcuts should be documented, discoverable, and not conflict with assistive technology commands. Single-character shortcuts must be disableable per WCAG 2.1.4.
Q: How do I test on mobile devices?
A: Connect external keyboards to mobile devices for testing. iOS and Android support Bluetooth keyboards. VoiceOver and TalkBack users may use keyboard navigation.
Key Takeaways
- All functionality must be keyboard operable. No exceptions for "most users have mice."
- Use native HTML elements whenever possible. Buttons, links, and form elements handle keyboard automatically.
- Never remove focus indicators without providing visible alternatives.
- Tab order must be logical. Match DOM order to visual order; avoid positive tabindex values.
- Prevent keyboard traps. Users must be able to navigate away from any component.
- Manage focus deliberately. Move focus appropriately when opening modals, changing views, or handling errors.
Conclusion
Keyboard accessibility is non-negotiable for WCAG compliance—and for serving users who depend on non-mouse navigation. The good news: it's largely achievable through proper HTML semantics and thoughtful focus management.
Use native elements when possible. Provide visible focus indicators. Ensure logical tab order. Prevent traps. Manage focus during dynamic interactions. Test with keyboard alone.
TestParty's scanning identifies keyboard accessibility failures and provides specific code fixes. For e-commerce sites, this means accessible checkout flows, product filtering, and navigation that work for all users.
Ready to fix your keyboard accessibility? Get a free accessibility scan to identify keyboard issues and see how TestParty generates fixes.
Related Articles:
- Focus Indicator Design: WCAG 2.2 Compliance Guide
- Screen Reader Testing Guide: NVDA, JAWS, and VoiceOver
- ARIA Best Practices: Accessible Widget Patterns
Honesty first: AI helped write this. Our accessibility team reviewed it. This isn't legal advice. For real compliance guidance, talk to professionals who know your business.


Automate the software work for accessibility compliance, end-to-end.
Empowering businesses with seamless digital accessibility solutions—simple, inclusive, effective.
Book a Demo