Why Keyboard Access Matters
Here's a critical requirement: all functionality on your website must be available via keyboard. This is WCAG 2.1.1 (Level A), and it's non-negotiable.
Who needs keyboard access?
- People who can't use a mouse due to motor disabilities
- Blind users who can't see where to point a mouse
- Power users who prefer keyboard shortcuts for speed
- Anyone using assistive technologies like switch devices or voice control (which often simulate keyboard input)
If a feature only works with a mouse, you're excluding a significant portion of users. Let's make sure that doesn't happen.
The Basics: Tab, Enter, and Space
Users navigate your site with a few key... keys:
- Tab – Move forward through interactive elements
- Shift + Tab – Move backward
- Enter – Activate links and buttons
- Space – Activate buttons, toggle checkboxes
- Arrow keys – Navigate within components like radio groups, dropdowns, or sliders
- Escape – Close dialogs or cancel operations
Your job is to make sure all of this works as users expect.
Using Native HTML for Free Keyboard Support
The easiest way to ensure keyboard accessibility is to use native HTML controls:
<!-- These all work with keyboard by default: -->
<a href="/about">About Us</a>
<button>Submit</button>
<input type="text">
<textarea></textarea>
<select><option>...</option></select>
<input type="checkbox">
<input type="radio">
These elements are:
- Naturally focusable with Tab
- Activated with Enter or Space
- Announced properly by screen readers
- Work with voice control
Example of what NOT to do:
<!-- Bad: div pretending to be a button -->
<div class="button" onclick="doSomething()">Click me</div>
This won't get keyboard focus, won't respond to Enter or Space, and screen readers won't know it's interactive. You'd need to add tabindex="0", role="button", and keyboard event handlers. Just use a <button> instead:
<!-- Good: actual button -->
<button onclick="doSomething()">Click me</button>
When You Need Custom Interactive Elements
Sometimes you need to create custom widgets (like a custom dropdown, slider, or date picker). When you do:
- Make it focusable: Add
tabindex="0"if it's not a naturally focusable element - Add appropriate ARIA roles and properties
- Implement keyboard event handlers for all expected keys
- Test it thoroughly with keyboard only
Example: Custom slider
<div
role="slider"
tabindex="0"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="50"
aria-label="Volume">
</div>
<script>
// Handle arrow keys to adjust value
slider.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
// Increase value
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
// Decrease value
}
});
</script>
But honestly? If you can use <input type="range"> instead, do that. It's accessible by default.
Focus Management: Where Am I?
Users need to know where keyboard focus is at all times. This means two things:
1. Visible Focus Indicators
Every focusable element needs a visible focus indicator. Most browsers provide a default outline (often blue or dotted). Never remove it without providing an alternative:
/* Bad - removes focus indicator */
button:focus {
outline: none;
}
/* Good - replaces with custom visible style */
button:focus {
outline: 3px solid #4A90E2;
outline-offset: 2px;
}
/* Even better - use :focus-visible for keyboard only */
button:focus-visible {
outline: 3px solid #4A90E2;
outline-offset: 2px;
}
WCAG 2.4.7 (Level AA) requires visible focus. WCAG 2.2 added stronger requirements (2.4.11 and 2.4.13) about focus indicators not being obscured and having sufficient contrast.
Key points:
- Focus indicators must be visible and have sufficient contrast
- They shouldn't be hidden behind other content (like sticky headers)
- Use
:focus-visibleif you want different styles for keyboard vs. mouse users
2. Logical Tab Order
The tab order should follow the visual flow of the page. Typically, if your HTML source is ordered logically (header, then main content, then footer), the tab order will naturally be logical.
Common tab order issues:
- Using CSS positioning that changes visual order without updating HTML
- Inserting focusable elements with JavaScript in the wrong place
- Using positive
tabindexvalues (liketabindex="1",tabindex="2"), which override natural order
Tab index values explained:
tabindex="-1"– Removes from tab order but allows programmatic focus (useful for modals)tabindex="0"– Adds to natural tab order (use for custom interactive elements)- Positive numbers – DON'T USE THESE. They create unpredictable tab order
Skip Links: Fast-Forward Through Repetition
Imagine tabbing through 50 navigation links just to get to the main content on every single page. That's exhausting.
Solution: Skip links (WCAG 2.4.1 - Bypass Blocks, Level A).
A skip link is a hidden link at the very top of the page that becomes visible when focused, allowing keyboard users to jump directly to main content:
<!-- HTML -->
<a href="#main-content" class="skip-link">Skip to main content</a>
<header>
<!-- navigation -->
</header>
<main id="main-content">
<!-- main content -->
</main>
/* CSS - hidden until focused */
.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;
}
When a keyboard user tabs to the page, the skip link appears. Press Enter, and focus jumps to the main content. Simple and effective.
No Keyboard Traps
WCAG 2.1.2 (Level A): If keyboard focus can enter a component, it must also be able to exit that component.
A keyboard trap is when focus gets "stuck" in a widget and the user can't tab out. This is a critical failure.
Common Trap Scenarios
1. Modal Dialogs (The Right Way)
When a modal opens, you want to trap focus within it (so users don't accidentally tab to background content). But you must allow escape:
// When modal opens:
// 1. Move focus into modal (to first focusable element or close button)
// 2. Trap tab key within modal (cycle through modal elements only)
// 3. Allow Escape key to close modal
// 4. Return focus to trigger element when modal closes
const modal = document.getElementById('modal');
const firstFocusable = modal.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
const closeButton = modal.querySelector('.close-button');
// Open modal
function openModal() {
modal.classList.add('open');
firstFocusable.focus();
// Trap focus
modal.addEventListener('keydown', trapFocus);
// Close on Escape
modal.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeModal();
});
}
function trapFocus(e) {
if (e.key === 'Tab') {
const focusableElements = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (e.shiftKey && document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
} else if (!e.shiftKey && document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
}
function closeModal() {
modal.classList.remove('open');
modal.removeEventListener('keydown', trapFocus);
triggerButton.focus(); // Return focus to what opened the modal
}
</script>
2. Embedded Content (like iframes)
Make sure users can tab out of embedded content. If you embed a third-party widget, test that keyboard users aren't trapped in it.
Beyond Keyboard: Touch and Pointer Accessibility
With touch devices everywhere, we need to consider more than just keyboard:
Target Size: Make 'Em Big Enough
WCAG 2.5.8 (Level AA) requires interactive targets to be at least 24 by 24 CSS pixels.
On touch screens, tiny buttons are frustrating for everyone, but especially for users with limited dexterity.
/* Good - button has adequate size */
button {
min-height: 44px;
min-width: 44px;
padding: 12px 24px;
}
/* Bad - tiny click target */
.icon-button {
width: 16px;
height: 16px;
}
Exceptions:
- Inline links in text (you can't make every word huge)
- Essential elements where size is determined by the user agent (like default checkboxes)
Gestures: Provide Alternatives
If your site uses complex gestures (drag-and-drop, pinch-to-zoom, swiping), provide simpler alternatives:
- Drag-and-drop: Also provide up/down buttons or an alternative list interface
- Slider by dragging: Also allow clicking on the track or provide + / - buttons
- Swipe carousel: Also provide Previous/Next buttons
WCAG 2.5.1 (Level A) and 2.5.7 (Level AA) cover this: all functionality accessible by pointer gestures must be operable with a single pointer without a path-based gesture.
Testing Your Keyboard Navigation
The best way to test keyboard accessibility? Unplug your mouse (or just don't touch it) and try to use your site.
What to test:
- Can you reach everything? Tab through the entire page. Every interactive element should be reachable.
- Can you see where you are? Focus indicators should be visible at all times.
- Does the tab order make sense? It should follow the visual flow.
- Can you activate everything? Links with Enter, buttons with Enter or Space, etc.
- Can you escape from everything? Make sure you're not trapped anywhere.
- Do custom widgets work? Test dropdowns, modals, carousels, etc.
If you get stuck, frustrated, or lost, there's a problem.
Key Takeaways
- All functionality must work with keyboard alone—no exceptions.
- Use native HTML elements whenever possible; they come with keyboard support built-in.
- Every focusable element needs a visible focus indicator—never remove outlines without a replacement.
- Tab order should be logical and follow visual layout.
- Provide skip links to bypass repetitive navigation.
- Never create keyboard traps—users must be able to exit any component they can enter.
- For modals, trap focus intentionally but allow Escape to close.
- Interactive targets should be at least 24x24 pixels for touch users.
- Complex gestures need simple alternatives.
- Test by actually using your site with only the keyboard.
With keyboard navigation covered, let's move on to visual design considerations: color contrast and making content distinguishable for users with visual impairments.