Accessibility (A11Y)
Keyboard Navigation & Focus Management
Make your website fully keyboard accessible. Learn focus management, tab order, keyboard shortcuts, and focus trapping for accessible web applications.
Why Keyboard Accessibility Matters
Many users rely on keyboards: motor impairments, blind users with screen readers, power users who prefer keyboard shortcuts, and users with broken/missing mouse.
- ✅ Screen reader users navigate with keyboard
- ✅ Motor impairment users may only use keyboard
- ✅ Power users prefer keyboard efficiency
- ✅ Temporary situations (broken mouse, remote access)
Focusable Elements
Naturally Focusable
These elements are focusable by default:
HTML
<!-- Naturally focusable elements -->
<a href="/page">Link</a>
<button>Button</button>
<input type="text">
<textarea></textarea>
<select></select>
<audio controls></audio>
<video controls></video>
<!-- Users can Tab to these elements -->Making Elements Focusable
HTML
<!-- Add tabindex="0" to make focusable -->
<div tabindex="0" role="button" onclick="doSomething()">
Custom Button
</div>
<!-- tabindex values: -->
<!-- 0: Natural tab order -->
<!-- -1: Programmatically focusable (not in tab order) -->
<!-- 1+: Custom tab order (avoid - confusing!) -->Removing from Tab Order
HTML
<!-- Remove from tab order -->
<button tabindex="-1">Not keyboard accessible</button>
<!-- Useful for: -->
<!-- - Disabled states -->
<!-- - Hidden content -->
<!-- - Programmatic focus only -->Focus Indicators
Always Show Focus
CSS
<style>
/* Default browser focus (usually blue outline) */
button:focus {
/* NEVER do this: */
/* outline: none; ⌠*/
}
/* ✅ Good: Custom but visible focus */
button:focus {
outline: 3px solid #4CAF50;
outline-offset: 2px;
}
/* Modern approach: focus-visible (keyboard only) */
button:focus-visible {
outline: 3px solid #4CAF50;
outline-offset: 2px;
}
/* Remove focus for mouse clicks only */
button:focus:not(:focus-visible) {
outline: none;
}
</style>Focus Styles for Different Elements
CSS
<style>
/* Links */
a:focus {
outline: 2px solid #2196F3;
outline-offset: 2px;
background-color: #E3F2FD;
}
/* Buttons */
button:focus {
outline: 3px solid #4CAF50;
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.3);
}
/* Inputs */
input:focus,
textarea:focus,
select:focus {
border-color: #2196F3;
box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.2);
outline: none;
}
/* Custom elements */
[tabindex]:focus {
outline: 2px dashed #FF9800;
outline-offset: 4px;
}
</style>Tab Order
Natural Tab Order (Best)
HTML
<!-- Tab order follows DOM order -->
<input type="text"> <!-- Tab order: 1 -->
<button>Submit</button> <!-- Tab order: 2 -->
<a href="/help">Help</a> <!-- Tab order: 3 -->
<!-- ✅ This is best - predictable and logical -->Custom Tab Order (Avoid)
HTML
<!-- ⌠Don't use positive tabindex values -->
<input type="text" tabindex="3">
<button tabindex="1">Submit</button>
<a href="/help" tabindex="2">Help</a>
<!-- Confusing! Tab order is now: Button → Link → Input -->
<!-- Very hard to maintain -->Keyboard Event Handling
Custom Interactive Elements
HTML
<div role="button"
tabindex="0"
onclick="handleClick()"
onkeydown="handleKeyDown(event)">
Custom Button
</div>
<script>
function handleKeyDown(event) {
// Enter or Space activates buttons
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleClick();
}
}
function handleClick() {
console.log('Button activated!');
}
</script>
<!-- Must handle both click AND keyboard events -->Common Keyboard Patterns
JAVASCRIPT
<script>
// Enter and Space for buttons
if (event.key === 'Enter' || event.key === ' ') {
button.click();
}
// Escape to close dialogs
if (event.key === 'Escape') {
closeDialog();
}
// Arrow keys for navigation
if (event.key === 'ArrowDown') {
focusNextItem();
}
if (event.key === 'ArrowUp') {
focusPreviousItem();
}
// Home/End for first/last
if (event.key === 'Home') {
focusFirstItem();
}
if (event.key === 'End') {
focusLastItem();
}
</script>Focus Trapping (Modals)
Trap Focus in Modal Dialog
HTML
<div role="dialog" aria-modal="true" id="myDialog">
<h2>Dialog Title</h2>
<button id="close-btn">Close</button>
<input type="text">
<button id="save-btn">Save</button>
</div>
<script>
const dialog = document.getElementById('myDialog');
const focusableElements = dialog.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
// Trap focus
dialog.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey) {
// Shift+Tab
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
// Tab
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
}
// Escape to close
if (e.key === 'Escape') {
closeDialog();
}
});
// Focus first element when dialog opens
firstElement.focus();
</script>Skip Links
HTML
<body>
<a href="#main-content" class="skip-link">
Skip to main content
</a>
<a href="#nav" class="skip-link">
Skip to navigation
</a>
<header>
<nav id="nav">
<!-- Many navigation links -->
</nav>
</header>
<main id="main-content">
<!-- Main content -->
</main>
</body>
<style>
.skip-link {
position: absolute;
top: -40px;
left: 0;
padding: 8px;
background: #000;
color: white;
text-decoration: none;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
</style>Managing Focus
Moving Focus Programmatically
HTML
<button onclick="openDialog()">Open Dialog</button>
<div id="dialog" hidden>
<h2 id="dialog-title">Dialog</h2>
<button id="dialog-close">Close</button>
</div>
<script>
let previousFocus;
function openDialog() {
// Store current focus
previousFocus = document.activeElement;
// Show dialog
dialog.hidden = false;
// Move focus to dialog
document.getElementById('dialog-close').focus();
}
function closeDialog() {
// Hide dialog
dialog.hidden = true;
// Return focus to trigger element
if (previousFocus) {
previousFocus.focus();
}
}
</script>Focus After Dynamic Content
HTML
<button onclick="loadMore()">Load More</button>
<div id="content"></div>
<script>
async function loadMore() {
const newContent = await fetchContent();
// Add new content
const newItem = document.createElement('article');
newItem.innerHTML = newContent;
newItem.tabIndex = -1; // Make focusable
content.appendChild(newItem);
// Move focus to new content
newItem.focus();
}
</script>Keyboard Shortcuts
HTML
<script>
// Global keyboard shortcuts
document.addEventListener('keydown', (e) => {
// Don't trigger if typing in input
if (e.target.matches('input, textarea')) return;
// Cmd/Ctrl + K: Search
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
openSearch();
}
// ? : Show keyboard shortcuts help
if (e.key === '?') {
showKeyboardHelp();
}
// / : Focus search
if (e.key === '/') {
e.preventDefault();
document.getElementById('search').focus();
}
});
</script>
<!-- Provide keyboard shortcuts guide -->
<dialog id="keyboard-help">
<h2>Keyboard Shortcuts</h2>
<dl>
<dt><kbd>Ctrl</kbd> + <kbd>K</kbd></dt>
<dd>Open search</dd>
<dt><kbd>?</kbd></dt>
<dd>Show this help</dd>
<dt><kbd>/</kbd></dt>
<dd>Focus search box</dd>
</dl>
</dialog>Testing Keyboard Accessibility
Manual Testing Checklist
- ☑ Unplug mouse and navigate entire site with keyboard
- ☑ Press Tab - can you reach all interactive elements?
- ☑ Press Shift+Tab - can you go backwards?
- ☑ Press Enter/Space on buttons - do they activate?
- ☑ Press Escape - do modals close?
- ☑ Are focus indicators always visible?
- ☑ Is tab order logical (left-to-right, top-to-bottom)?
- ☑ Can you access dropdown menus?
- ☑ Can you submit forms?
- ☑ Can you close overlays/modals?
Common Keyboard Issues
- ⌠Removed focus outline (outline: none)
- ⌠Div/span "buttons" without keyboard support
- ⌠Custom dropdowns not keyboard accessible
- ⌠Modals without focus trapping
- ⌠Click-only interactions (no keyboard handler)
- ⌠Content that appears on hover only
- ⌠Skip links missing
Best Practices
✅ Do
- Use semantic HTML (button, a, input)
- Always show focus indicators
- Use tabindex="0" for custom controls
- Handle both click and keyboard events
- Trap focus in modals
- Return focus after closing dialogs
- Provide skip links
- Test with keyboard only
- Use logical tab order
⌠Don't
- Remove focus outlines
- Use positive tabindex values
- Create keyboard traps (except modals)
- Rely on mouse-only interactions
- Use div/span for buttons
- Forget to handle Escape key
- Make custom controls without keyboard support
- Skip keyboard testing