Keyboard Navigation
What This Means
Keyboard navigation ensures that all website functionality can be accessed and operated using only a keyboard, without requiring a mouse or trackpad. This is essential for users who cannot use a mouse due to motor disabilities, vision impairments, or those who simply prefer keyboard navigation for efficiency.
Impact on Your Business
Legal Compliance:
- Keyboard accessibility is required under WCAG 2.1 Level A
- One of the most fundamental accessibility requirements
- Failure to support keyboard navigation is a critical violation
- Required for ADA and Section 508 compliance
User Populations Affected:
- Users with motor disabilities - Cannot use a mouse
- Blind and low-vision users - Navigate with screen readers
- Power users - Prefer keyboard for efficiency
- Temporary disabilities - Broken arm, RSI
- Potentially millions of users excluded
Business Impact:
- Keyboard-inaccessible sites exclude entire user segments
- Critical functionality (checkout, forms) may be unusable
- Increases bounce rates for affected users
- Damages brand reputation and trust
SEO and Technical Benefits:
- Semantic HTML improves SEO
- Better code structure and maintainability
- Improved compatibility with assistive technologies
- Benefits mobile and touch device users
How to Diagnose
Method 1: Manual Keyboard Testing (Recommended)
This is the most important test:
Close or ignore your mouse/trackpad
Navigate using only keyboard:
Tab- Move to next interactive elementShift+Tab- Move to previous interactive elementEnter- Activate links and buttonsSpace- Activate buttons, check checkboxesArrow keys- Navigate within components (dropdowns, radio groups)Esc- Close modals and menus
Check every page section:
- Can you reach all links?
- Can you activate all buttons?
- Can you use all forms?
- Can you close modals?
- Can you use dropdown menus?
What to Look For:
- Elements you cannot reach with Tab
- Missing or invisible focus indicators
- Illogical tab order
- Keyboard traps (can't Tab out)
- Elements that require mouse (hover-only)
Method 2: WAVE Browser Extension
- Install WAVE Extension
- Navigate to your webpage
- Click WAVE icon
- Review errors:
- Check for structural issues:
- "Heading" hierarchy
- "Landmark" regions
What to Look For:
- Links without text
- Buttons without labels
- Form inputs without labels
- Broken heading hierarchy
- Missing ARIA labels
Method 3: axe DevTools
- Install axe DevTools Extension
- Open Chrome DevTools (
F12) - Navigate to "axe DevTools" tab
- Click "Scan ALL of my page"
- Review keyboard-related violations:
- "Elements must have sufficient color contrast"
- "Links must have discernible text"
- "Buttons must have discernible text"
- "Form elements must have labels"
What to Look For:
- Interactive elements without accessible names
- Missing focus indicators
- Keyboard trap issues
- ARIA role violations
Method 4: Chrome DevTools Accessibility Tab
- Open your website
- Press
F12to open DevTools - Right-click an element → Inspect
- In Elements panel, click "Accessibility" tab
- Review:
- Computed Properties - Name, Role, Focusable
- Accessibility Tree - How element appears to assistive tech
What to Look For:
- Focusable: "false" on interactive elements
- Missing accessible name
- Incorrect role
- Elements not in accessibility tree
Method 5: Focus Indicator Test
- Enable Chrome DevTools > Rendering
- Check "Emulate a focused page"
- Tab through the page
- Observe focus indicators
What to Look For:
- Invisible focus indicators (no visual change)
- Focus indicators removed by CSS
- Low contrast focus indicators
- Focus jumping unexpectedly
General Fixes
Fix 1: Make All Interactive Elements Focusable
Ensure elements can receive keyboard focus:
Use proper HTML elements:
<!-- Bad - div is not focusable by default --> <div onclick="handleClick()">Click me</div> <!-- Good - button is focusable --> <button onclick="handleClick()">Click me</button>If you must use non-button elements, add tabindex and role:
<!-- Only do this if you can't use a real button --> <div role="button" tabindex="0" onclick="handleClick()" onkeydown="handleKeyDown(event)" > Click me </div> <script> function handleKeyDown(event) { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); handleClick(); } } </script>Use links for navigation, buttons for actions:
<!-- Link - goes somewhere --> <a href="/products">View Products</a> <!-- Button - does something --> <button type="button" onclick="openModal()">Open Details</button>
Fix 2: Provide Visible Focus Indicators
Make it obvious which element has focus:
Never remove focus outlines without replacement:
/* Bad - removes focus with no replacement */ *:focus { outline: none; } /* Good - custom focus indicator */ button:focus, a:focus, input:focus { outline: 2px solid #0066cc; outline-offset: 2px; }Use high contrast focus styles:
/* Ensure focus visible on all backgrounds */ :focus { outline: 2px solid #0066cc; outline-offset: 2px; } /* For dark backgrounds */ .dark-theme :focus { outline: 2px solid #66b3ff; }Use :focus-visible for mouse vs keyboard:
/* Only show outline for keyboard focus */ button:focus-visible { outline: 2px solid #0066cc; outline-offset: 2px; } /* Remove outline for mouse clicks */ button:focus:not(:focus-visible) { outline: none; }Enhanced focus styles:
button:focus { outline: 2px solid #0066cc; outline-offset: 2px; box-shadow: 0 0 0 4px rgba(0, 102, 204, 0.2); }
Fix 3: Fix Tab Order
Ensure logical navigation sequence:
Use natural DOM order (no tabindex > 0):
<!-- Bad - forces unnatural order --> <button tabindex="3">Third</button> <button tabindex="1">First</button> <button tabindex="2">Second</button> <!-- Good - natural DOM order --> <button>First</button> <button>Second</button> <button>Third</button>Use tabindex values correctly:
tabindex="0"- Natural tab order (focusable)tabindex="-1"- Programmatically focusable (not in tab order)tabindex="1+"- Avoid! Creates unpredictable order
Structure HTML in reading order:
<!-- Good - header, nav, main, footer --> <header> <nav><!-- navigation --></nav> </header> <main> <h1>Page Title</h1> <!-- main content --> </main> <footer><!-- footer --></footer>
Fix 4: Prevent Keyboard Traps
Users must be able to navigate away from all elements:
Test modals for keyboard traps:
// Trap focus within modal function trapFocus(element) { const focusableElements = element.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); const firstFocusable = focusableElements[0]; const lastFocusable = focusableElements[focusableElements.length - 1]; element.addEventListener('keydown', (e) => { if (e.key !== 'Tab') return; if (e.shiftKey) { // Shift + Tab if (document.activeElement === firstFocusable) { lastFocusable.focus(); e.preventDefault(); } } else { // Tab if (document.activeElement === lastFocusable) { firstFocusable.focus(); e.preventDefault(); } } }); }Ensure modals can be closed with Escape:
modal.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeModal(); } });Return focus after modal closes:
function openModal() { lastFocusedElement = document.activeElement; modal.style.display = 'block'; modal.querySelector('button').focus(); } function closeModal() { modal.style.display = 'none'; lastFocusedElement.focus(); }
Fix 5: Make Dropdowns and Menus Keyboard Accessible
Hover-only menus exclude keyboard users:
Add keyboard event handlers:
const menuButton = document.querySelector('.menu-button'); const menu = document.querySelector('.menu'); // Show on click menuButton.addEventListener('click', () => { menu.classList.toggle('open'); }); // Show on Enter/Space menuButton.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); menu.classList.toggle('open'); } }); // Navigate menu items with arrow keys menu.addEventListener('keydown', (e) => { const items = menu.querySelectorAll('a'); const currentIndex = Array.from(items).indexOf(document.activeElement); if (e.key === 'ArrowDown') { e.preventDefault(); const next = items[currentIndex + 1] || items[0]; next.focus(); } else if (e.key === 'ArrowUp') { e.preventDefault(); const prev = items[currentIndex - 1] || items[items.length - 1]; prev.focus(); } else if (e.key === 'Escape') { menu.classList.remove('open'); menuButton.focus(); } });Use ARIA for dropdown state:
<button aria-expanded="false" aria-controls="dropdown-menu" onclick="toggleMenu()" > Menu </button> <ul id="dropdown-menu" hidden> <li><a href="#1">Item 1</a></li> <li><a href="#2">Item 2</a></li> </ul> <script> function toggleMenu() { const button = document.querySelector('[aria-controls="dropdown-menu"]'); const menu = document.getElementById('dropdown-menu'); const isExpanded = button.getAttribute('aria-expanded') === 'true'; button.setAttribute('aria-expanded', !isExpanded); menu.hidden = isExpanded; } </script>
Fix 6: Label All Form Elements
Every input needs an associated label:
Use explicit labels:
<!-- Good - explicit association --> <label for="email">Email Address</label> <input id="email" type="email" name="email">Or wrap inputs in labels:
<!-- Also good - implicit association --> <label> Email Address <input type="email" name="email"> </label>Use aria-label for icon buttons:
<button aria-label="Close modal"> <svg aria-hidden="true"><!-- close icon --></svg> </button>Group related form elements:
<fieldset> <legend>Shipping Address</legend> <label for="street">Street</label> <input id="street" type="text"> <label for="city">City</label> <input id="city" type="text"> </fieldset>
Fix 7: Add Skip Links
Allow users to skip repetitive content:
Add skip to main content link:
<body> <a href="#main" class="skip-link">Skip to main content</a> <header> <nav><!-- Navigation --></nav> </header> <main id="main" tabindex="-1"> <!-- Main content --> </main> </body>Style skip link (visible on focus):
.skip-link { position: absolute; top: -40px; left: 0; background: #000; color: #fff; padding: 8px; text-decoration: none; z-index: 100; } .skip-link:focus { top: 0; }
Fix 8: Use Semantic HTML and ARIA
Proper structure aids navigation:
Use landmark regions:
<header role="banner"> <nav role="navigation" aria-label="Main navigation"> <!-- nav items --> </nav> </header> <main role="main"> <article> <h1>Article Title</h1> <!-- content --> </article> </main> <aside role="complementary" aria-label="Related articles"> <!-- sidebar --> </aside> <footer role="contentinfo"> <!-- footer --> </footer>Use proper heading hierarchy:
<h1>Page Title</h1> <h2>Section 1</h2> <h3>Subsection 1.1</h3> <h3>Subsection 1.2</h3> <h2>Section 2</h2> <h3>Subsection 2.1</h3>Use ARIA when needed:
<!-- Live region for dynamic updates --> <div role="status" aria-live="polite"> Items added to cart </div> <!-- Current page in navigation --> <nav> <a href="/">Home</a> <a href="/products" aria-current="page">Products</a> <a href="/about">About</a> </nav>
Platform-Specific Guides
Detailed implementation instructions for your specific platform:
Verification
After implementing keyboard accessibility:
Complete keyboard navigation test:
- Unplug mouse
- Navigate entire site with keyboard only
- Verify all functionality accessible
- Check tab order is logical
- Confirm focus always visible
Test with screen reader:
- NVDA (Windows) or VoiceOver (Mac)
- Navigate using screen reader shortcuts
- Verify all elements announced correctly
- Check ARIA labels make sense
Run automated tests:
- axe DevTools (no keyboard violations)
- WAVE extension (no structural errors)
- Lighthouse (passes accessibility audit)
Test specific scenarios:
- Complete a form submission
- Open and close a modal
- Navigate a dropdown menu
- Use any interactive widgets
Test edge cases:
- Keyboard traps (shouldn't exist)
- Skip links work
- Focus management in SPAs
- Dynamic content updates
Common Mistakes
- Using
<div>or<span>instead of<button>- Not focusable by default - Removing focus outlines - Users can't see where they are
- Hover-only menus - Keyboard users can't access
- Missing keyboard handlers - Only mouse events implemented
- Improper tabindex use - Using tabindex > 0
- Keyboard traps - Can't escape modal or widget
- Unlabeled form inputs - Screen readers can't identify purpose
- Missing skip links - Forces navigation through all content
- Illogical tab order - Visual order doesn't match DOM order
- Focus not visible - Low contrast or invisible focus indicators
Keyboard Navigation Checklist
- All interactive elements reachable via Tab
- Tab order is logical and follows visual order
- Focus indicators are clearly visible
- Focus indicators have sufficient contrast (3:1)
- Enter/Space activate buttons and links
- Escape closes modals and menus
- Arrow keys work in dropdowns and menus
- No keyboard traps exist
- Skip links provided for main content
- All form inputs have labels
- Custom widgets have appropriate ARIA
- Modals manage focus correctly
- Dynamic content updates announced to screen readers
- Dropdowns accessible without mouse
- All functionality available via keyboard