A systematic framework for testing keyboard accessibility across all interactive components. For each component type, this matrix defines the expected keyboard behavior, the keys to test, and pass/fail criteria.
1. Identify all interactive components on the page you're testing
2. For each component, find its row in the matrix below
3. Test each keyboard interaction listed
4. Mark as Pass, Fail, or N/A
5. For failures, note the specific issue and which element is affected
These apply to every page, regardless of what components are present.
| Test | Keys | Expected Behavior | Pass/Fail |
|---|---|---|---|
| Tab through all elements | Tab | Focus moves forward through all interactive elements in visual reading order | ☐ |
| Reverse tab | Shift+Tab | Focus moves backward through interactive elements | ☐ |
| Skip navigation | Tab (first press) | First focusable element is a "Skip to content" link | ☐ |
| Skip link works | Enter on skip link | Focus moves to main content area | ☐ |
| No keyboard traps | Tab repeatedly | Focus eventually returns to the browser chrome (URL bar); never gets stuck | ☐ |
| Focus visible | Tab through all | Every focused element shows a visible focus indicator (outline, ring, highlight) | ☐ |
| Focus order logical | Tab through all | Focus order matches visual reading order (left-to-right, top-to-bottom in LTR) | ☐ |
| No focus jumps | Interact with elements | Focus doesn't jump to unexpected locations after interactions | ☐ |
| Test | Keys | Expected Behavior | Pass/Fail |
|---|---|---|---|
| Focus on link | Tab | Link receives visible focus indicator | ☐ |
| Activate link | Enter | Link activates (navigates to destination or triggers action) | ☐ |
| Link text | N/A (inspect) | Link text describes the destination, not "click here" or "read more" | ☐ |
| Test | Keys | Expected Behavior | Pass/Fail |
|---|---|---|---|
| Focus on button | Tab | Button receives visible focus indicator | ☐ |
| Activate button | Enter | Button activates its action | ☐ |
| Activate button | Space | Button activates its action (both Enter and Space should work) | ☐ |
| Toggle button | Space | Toggles state and announces new state (aria-pressed changes) | ☐ |
| Disabled button | Tab | Disabled buttons should either be skipped in tab order OR be focusable but inoperable | ☐ |
| Test | Keys | Expected Behavior | Pass/Fail |
|---|---|---|---|
| Focus on input | Tab | Input receives focus; label is announced by screen reader | ☐ |
| Type in input | Character keys | Characters appear in the input field | ☐ |
| Select all text | Ctrl+A | All text in the field is selected | ☐ |
| Copy/paste | Ctrl+C / Ctrl+V | Standard clipboard operations work | ☐ |
| Move focus away | Tab / Shift+Tab | Focus leaves the input without submitting the form | ☐ |
| Required field | Tab away empty | Error message appears and is associated with the field | ☐ |
| Test | Keys | Expected Behavior | Pass/Fail |
|---|---|---|---|
| Focus on checkbox | Tab | Checkbox receives visible focus indicator | ☐ |
| Toggle checkbox | Space | Checkbox toggles between checked and unchecked | ☐ |
| Group navigation | Tab | Tab moves between checkbox groups (not between individual checkboxes in a group) | ☐ |
| Indeterminate state | Space | If indeterminate, toggles to checked or unchecked | ☐ |
| Test | Keys | Expected Behavior | Pass/Fail |
|---|---|---|---|
| Focus into group | Tab | Focus enters the radio group on the selected option (or first if none selected) | ☐ |
| Move within group | Arrow Up/Down | Focus and selection move between options | ☐ |
| Move within group | Arrow Left/Right | Same as Up/Down (horizontal radio groups) | ☐ |
| Wrap around | Arrow Down on last | Focus wraps to the first option | ☐ |
| Leave group | Tab | Focus leaves the radio group entirely (moves to next component) | ☐ |
| Test | Keys | Expected Behavior | Pass/Fail |
|---|---|---|---|
| Focus on select | Tab | Select element receives visible focus | ☐ |
| Open dropdown | Space / Enter / Alt+Down | Dropdown opens showing options | ☐ |
| Navigate options | Arrow Up/Down | Moves through options | ☐ |
| Select option | Enter | Selects the highlighted option and closes the dropdown | ☐ |
| Close without selecting | Escape | Dropdown closes; previous selection retained | ☐ |
| Type-ahead | Character key | Jumps to the first option starting with that character | ☐ |
| Test | Keys | Expected Behavior | Pass/Fail |
|---|---|---|---|
| Focus on trigger | Tab | Trigger button/input receives visible focus | ☐ |
| Open listbox | Enter / Space / Arrow Down | Listbox appears; first option (or previously selected) is highlighted | ☐ |
| Navigate options | Arrow Up/Down | Highlight moves through options | ☐ |
| Select option | Enter | Option is selected; listbox closes; trigger announces selection | ☐ |
| Close listbox | Escape | Listbox closes; focus returns to trigger; no selection change | ☐ |
| First/last option | Home / End | Highlight jumps to first/last option | ☐ |
| Type-ahead | Character key | Highlight moves to next option starting with that character | ☐ |
| aria-activedescendant | Arrow keys | aria-activedescendant on the input updates to the highlighted option's ID | ☐ |
| Test | Keys | Expected Behavior | Pass/Fail |
|---|---|---|---|
| Focus into tabs | Tab | Focus lands on the active tab (not the first tab if another is selected) | ☐ |
| Switch tabs | Arrow Left/Right | Focus and activation move between tabs | ☐ |
| First tab | Home | Focus and activation move to the first tab | ☐ |
| Last tab | End | Focus and activation move to the last tab | ☐ |
| Wrap around | Arrow Right on last | Focus wraps to the first tab | ☐ |
| Enter panel | Tab (from tab) | Focus moves into the active tab panel content | ☐ |
| Leave tabs | Shift+Tab (from panel) | Focus returns to the tab list | ☐ |
| Panel visibility | Arrow keys | Only the active panel is visible; others are hidden or display: none | ☐ |
| Test | Keys | Expected Behavior | Pass/Fail |
|---|---|---|---|
| Open modal | Enter/Space on trigger | Modal opens; focus moves to the first focusable element inside (or the close button) | ☐ |
| Close modal | Escape | Modal closes; focus returns to the trigger element | ☐ |
| Focus trap (forward) | Tab on last element | Focus wraps to the first focusable element in the modal | ☐ |
| Focus trap (backward) | Shift+Tab on first element | Focus wraps to the last focusable element in the modal | ☐ |
| Background inert | Tab repeatedly | Focus never leaves the modal while it's open | ☐ |
| Close via button | Enter/Space on close button | Modal closes; focus returns to trigger | ☐ |
| Background click | Click backdrop | Modal closes (optional but common); focus returns to trigger | ☐ |
| Scroll lock | Arrow keys / scroll | Background page doesn't scroll while modal is open | ☐ |
| Test | Keys | Expected Behavior | Pass/Fail |
|---|---|---|---|
| Focus on trigger | Tab | Disclosure trigger (button) receives visible focus | ☐ |
| Toggle open | Enter or Space | Content is revealed; aria-expanded changes to "true" | ☐ |
| Toggle closed | Enter or Space again | Content is hidden; aria-expanded changes to "false" | ☐ |
| Navigate to content | Tab | Focus moves into the revealed content (if it contains focusable elements) | ☐ |
| Accordion: single open | Enter on closed item | The clicked item opens; other items close | ☐ |
| Test | Keys | Expected Behavior | Pass/Fail |
|---|---|---|---|
| Focus on button | Tab | Menu button receives visible focus | ☐ |
| Open menu | Enter / Space / Arrow Down | Menu opens; focus moves to the first menu item | ☐ |
| Open at end | Arrow Up | Menu opens; focus moves to the last menu item | ☐ |
| Navigate items | Arrow Up/Down | Focus moves through menu items | ☐ |
| Activate item | Enter | Item action is triggered; menu closes; focus returns to trigger | ☐ |
| Close menu | Escape | Menu closes; focus returns to the trigger button | ☐ |
| Close and move | Tab | Menu closes; focus moves to the next element after the menu button | ☐ |
| First/last item | Home / End | Focus moves to first/last menu item | ☐ |
| Type-ahead | Character key | Focus moves to the next item starting with that character | ☐ |
| Disabled items | Arrow Down | Focus skips disabled items | ☐ |
| Test | Keys | Expected Behavior | Pass/Fail |
|---|---|---|---|
| Show tooltip | Focus (Tab to element) | Tooltip appears when the trigger receives focus | ☐ |
| Hide tooltip | Escape | Tooltip hides (WCAG 1.4.13 — dismissable) | ☐ |
| Hide on blur | Tab away | Tooltip hides when focus leaves the trigger | ☐ |
| Not hoverable required | N/A | Tooltip content is available without needing to hover over it | ☐ |
| Test | Keys | Expected Behavior | Pass/Fail |
|---|---|---|---|
| Focus on slider | Tab | Slider thumb receives visible focus | ☐ |
| Increment | Arrow Right / Arrow Up | Value increases by one step | ☐ |
| Decrement | Arrow Left / Arrow Down | Value decreases by one step | ☐ |
| Large increment | Page Up | Value increases by a larger step (typically 10%) | ☐ |
| Large decrement | Page Down | Value decreases by a larger step | ☐ |
| Set to minimum | Home | Value jumps to minimum | ☐ |
| Set to maximum | End | Value jumps to maximum | ☐ |
| Value announced | Any change | aria-valuenow, aria-valuetext are updated and announced | ☐ |
| Test | Keys | Expected Behavior | Pass/Fail |
|---|---|---|---|
| Keyboard alternative exists | N/A | Drag-and-drop has a keyboard-accessible alternative (move up/down buttons, select+move) | ☐ |
| Reorder with buttons | Enter on move button | Item moves in the specified direction | ☐ |
| Announce new position | After move | Screen reader announces the item's new position ("Item 3 of 5") | ☐ |
| Test | Keys | Expected Behavior | Pass/Fail |
|---|---|---|---|
| Previous/Next buttons | Tab + Enter | Previous and Next buttons are keyboard accessible | ☐ |
| Slide indicators | Tab + Enter | Individual slide indicators (dots) are keyboard accessible | ☐ |
| Pause auto-play | Enter on pause button | Auto-rotating carousel can be paused | ☐ |
| Swipe alternative | Arrow Left/Right or buttons | Touch swipe has a keyboard/button alternative | ☐ |
| Test | Keys | Expected Behavior | Pass/Fail |
|---|---|---|---|
| Focus on sort button | Tab | Column header sort button receives focus | ☐ |
| Activate sort | Enter or Space | Table re-sorts by that column; sort direction announced | ☐ |
| Navigate cells | Arrow keys (with screen reader) | Screen reader announces row/column headers as you move between cells | ☐ |
# Keyboard Navigation Audit
**Page:** [URL]
**Date:** [Date]
**Tester:** [Name]
**Browser:** [Browser and version]
## Results
| Component | Location | Keys Tested | Result | Notes |
|---|---|---|---|---|
| Skip link | Top of page | Tab, Enter | PASS | Jumps to #main |
| Main nav | Header | Tab, Enter, Arrow keys | FAIL | No arrow key support between items |
| Search form | Header | Tab, Enter | PASS | Labels announced correctly |
| ... | ... | ... | ... | ... |
## Failures
### [Component] — [Location]
**Issue:** [Description]
**Expected:** [What should happen]
**Actual:** [What happens instead]
**WCAG:** [Criterion number]
**Fix:** [Recommendation]Part of Frontend Developer Pro
How to fix the most common accessibility issues, with before/after code examples. Organized by issue frequency — the issues you'll encounter most often are listed first.
WCAG: 1.1.1 Non-text Content (Level A)
Frequency: Found on nearly every site audited
Images without alt attributes, or with unhelpful alt text like alt="image" or alt="IMG_4521.jpg".
<!-- No alt attribute — screen reader announces the filename -->
<img src="team-photo-2024.jpg">
<!-- Useless alt text -->
<img src="chart.png" alt="image">
<img src="hero-banner.jpg" alt="banner">
<!-- Alt text that repeats adjacent text -->
<h2>Meet Our Team</h2>
<img src="team.jpg" alt="Meet Our Team"><!-- Informative image: describe what the image communicates -->
<img src="team-photo-2024.jpg"
alt="The engineering team celebrating the product launch, gathered in the office with confetti">
<!-- Chart: describe the data, not the visual -->
<img src="chart.png"
alt="Bar chart showing monthly revenue: January $45K, February $52K, March $61K, trending upward">
<!-- Decorative image: empty alt so screen readers skip it -->
<img src="decorative-divider.svg" alt="">
<!-- Adjacent text already describes the image -->
<h2>Meet Our Team</h2>
<img src="team.jpg" alt=""> <!-- Empty because the heading already says it -->Is the image purely decorative (border, spacer, background)?
→ Yes: alt=""
→ No: Does adjacent text already describe the image?
→ Yes: alt=""
→ No: Does the image convey information?
→ Yes: Describe the information, not the image appearance
"Revenue chart showing 15% growth" not "colorful bar chart"
→ No: Is it a functional image (button, link)?
→ Yes: Describe the action: alt="Search" alt="Close dialog"
→ No: Describe the content briefly
WCAG: 1.3.1 Info and Relationships (Level A), 3.3.2 Labels or Instructions (Level A)
Frequency: Extremely common — especially in modern "minimal" designs
Form inputs without elements, relying on placeholder text instead.
<!-- Placeholder is NOT a label — it disappears when you start typing -->
<input type="email" placeholder="Email address">
<!-- No label, no placeholder — completely unlabeled -->
<input type="text" name="search">
<!-- Label exists but isn't connected to the input -->
<p>Email Address</p>
<input type="email" id="email"><!-- Method 1: Explicit <label> with for/id (preferred) -->
<label for="email">Email Address</label>
<input type="email" id="email" placeholder="user@example.com">
<!-- Method 2: Wrapping label (no for/id needed) -->
<label>
Email Address
<input type="email" placeholder="user@example.com">
</label>
<!-- Method 3: aria-label (when visible label isn't desirable) -->
<input type="search" aria-label="Search articles" placeholder="Search...">
<!-- Method 4: aria-labelledby (label text exists elsewhere on page) -->
<h2 id="search-heading">Search Articles</h2>
<input type="search" aria-labelledby="search-heading">1. Visible — best for all users (sighted + screen reader)
2. aria-label — when a visible label would clutter the design
3. aria-labelledby — when the label text exists elsewhere
4. title — last resort (some screen readers don't announce it reliably)
WCAG: 1.4.3 Contrast Minimum (Level AA)
Frequency: Found on most sites, especially with light gray text
Text colors that don't have enough contrast against their background.
| Text Size | Minimum Contrast Ratio |
|---|---|
| Normal text (< 18px, or < 14px bold) | 4.5:1 |
| Large text (≥ 18px, or ≥ 14px bold) | 3:1 |
| UI components and graphics | 3:1 |
/* FAIL: Light gray on white — ratio 2.6:1 */
.subtitle { color: #aaaaaa; background: #ffffff; }
/* FAIL: Medium gray on white — ratio 3.5:1 */
.body-text { color: #999999; background: #ffffff; }
/* FAIL: White text on light blue — ratio 2.9:1 */
.cta-btn { color: #ffffff; background: #74b9ff; }
/* FAIL: Placeholder text too light — ratio 2.3:1 */
input::placeholder { color: #cccccc; }/* PASS: Dark gray on white — ratio 7.0:1 */
.subtitle { color: #666666; background: #ffffff; }
/* PASS: Darker gray on white — ratio 4.6:1 */
.body-text { color: #767676; background: #ffffff; }
/* PASS: White on darker blue — ratio 4.7:1 */
.cta-btn { color: #ffffff; background: #2d7dd2; }
/* PASS: Darker placeholder — ratio 4.6:1 */
input::placeholder { color: #767676; }WCAG: 1.3.1 Info and Relationships (Level A), 2.4.6 Headings and Labels (Level AA)
Frequency: Very common — styled WCAG: 2.1.1 Keyboard (Level A) Frequency: Common with custom JavaScript components Use native HTML elements whenever possible. If you must use a custom element, add ALL of: 1. 2. 3. 4. Visual focus indicator WCAG: 2.4.7 Focus Visible (Level AA) Frequency: Extremely common — developers remove outlines for aesthetics WCAG: 4.1.2 Name, Role, Value (Level A) Frequency: Common in SPAs with custom widgets WCAG: 4.1.3 Status Messages (Level AA) Frequency: Common in dynamic web applications WCAG: 1.4.5 Images of Text (Level AA) Frequency: Common in marketing pages and hero sections Why: Real text can be resized by the user, reflows on mobile, works with high contrast mode, and is readable by screen readers without relying on alt text. WCAG: 3.1.1 Language of Page (Level A) Frequency: Overlooked on many sites When you have a long list of issues, fix them in this order: Issues that prevent users from accessing content or functionality: Issues that severely degrade the experience: Issues that cause inconvenience: Issues that are technically violations but have minimal impact: Part of Frontend Developer Pro Get the full Accessibility Audit Toolkit and unlock everything. Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices. Access all interactive tools with complete data, all workload profiles, and the full scenario library. Downloadable source code, configuration files, and working examples from every chapter. Free updates for life. Every new chapter, tool, and improvement included. elements instead of real headings
Before (Failing)
<!-- Visual heading but not a real heading element -->
<div class="section-title" style="font-size: 24px; font-weight: bold;">
Our Services
</div>
<!-- Skipped heading level: h1 → h4 -->
<h1>Acme Corp</h1>
<h4>Contact Us</h4> <!-- Skipped h2 and h3 -->
<!-- Multiple h1 elements -->
<h1>Welcome</h1>
<h1>Features</h1> <!-- Page should have only one h1 -->After (Passing)
<!-- Use real heading elements -->
<h2>Our Services</h2>
<!-- Logical hierarchy: h1 → h2 → h3 -->
<h1>Acme Corp</h1>
<h2>About Us</h2>
<h2>Contact Us</h2>
<h3>Email</h3>
<h3>Phone</h3>
<!-- Single h1 per page -->
<h1>Welcome to Acme Corp</h1>
<h2>Features</h2>
5. Missing Keyboard Access
Before (Failing)
<!-- div with click handler — not keyboard accessible -->
<div class="card" onclick="openDetails()">
View Details
</div>
<!-- Span pretending to be a link -->
<span class="fake-link" onclick="navigate('/about')">About Us</span>
<!-- Custom toggle with no keyboard support -->
<div class="toggle-switch" onclick="toggle()">
<div class="toggle-thumb"></div>
</div>After (Passing)
<!-- Use a real button -->
<button class="card-action" onclick="openDetails()">
View Details
</button>
<!-- Use a real link -->
<a href="/about">About Us</a>
<!-- Button with ARIA states for toggle -->
<button class="toggle-switch"
role="switch"
aria-checked="false"
onclick="toggle(this)">
<span class="toggle-thumb"></span>
<span class="sr-only">Dark mode</span>
</button>Key Principle
, , , all have built-in keyboard support, focus management, and screen reader semantics. Custom elements have none of these.
tabindex="0" — makes it focusablerole="button" (or appropriate role)keydown handler for Enter and Space
6. No Focus Indicator
Before (Failing)
/* The most common accessibility violation on the web */
*:focus {
outline: none;
}
/* Or hidden by framework defaults */
button:focus {
outline: 0;
box-shadow: none;
}After (Passing)
/* Custom focus indicator that's visible and attractive */
:focus-visible {
outline: 3px solid #4a90d9;
outline-offset: 2px;
}
/* Remove outline only for mouse clicks, keep for keyboard */
:focus:not(:focus-visible) {
outline: none;
}
/* High contrast focus for dark backgrounds */
.dark-section :focus-visible {
outline: 3px solid #ffffff;
outline-offset: 2px;
}:focus-visible vs :focus:focus — Applies whenever the element has focus (mouse click, keyboard, programmatic):focus-visible — Applies only when the browser determines focus should be visible (keyboard navigation):focus-visible for styling — it shows the focus ring for keyboard users but hides it for mouse clicks
7. Missing ARIA for Custom Components
Before (Failing)
<!-- Custom dropdown with no ARIA -->
<div class="dropdown">
<div class="dropdown-toggle" onclick="toggleMenu()">
Select option ▾
</div>
<div class="dropdown-menu" style="display: none;">
<div class="dropdown-item" onclick="select('a')">Option A</div>
<div class="dropdown-item" onclick="select('b')">Option B</div>
<div class="dropdown-item" onclick="select('c')">Option C</div>
</div>
</div>After (Passing)
<!-- Custom dropdown with proper ARIA -->
<div class="dropdown">
<button class="dropdown-toggle"
aria-haspopup="listbox"
aria-expanded="false"
aria-controls="options-listbox"
id="dropdown-label">
Select option ▾
</button>
<ul role="listbox"
id="options-listbox"
aria-labelledby="dropdown-label"
hidden>
<li role="option" aria-selected="false" id="opt-a">Option A</li>
<li role="option" aria-selected="false" id="opt-b">Option B</li>
<li role="option" aria-selected="false" id="opt-c">Option C</li>
</ul>
</div>
8. Missing Live Region Announcements
Before (Failing)
<!-- Toast notification appears visually but screen readers don't know -->
<div class="toast">Item added to cart</div>
<!-- Search results count updates silently -->
<div class="results-count">Showing 12 results</div>
<!-- Form error appears but isn't announced -->
<div class="error-message">Email address is invalid</div>After (Passing)
<!-- Toast with aria-live="polite" — announced when content changes -->
<div class="toast" role="status" aria-live="polite">
Item added to cart
</div>
<!-- Search results count as live region -->
<div class="results-count" role="status" aria-live="polite" aria-atomic="true">
Showing 12 results
</div>
<!-- Form error as alert — announced immediately (assertive) -->
<div class="error-message" role="alert" aria-live="assertive">
Email address is invalid. Enter an address like user@example.com
</div>aria-live Values
Value When to Use politeStatus messages, search results, non-urgent updates — announced after the screen reader finishes current speech assertiveError messages, urgent alerts — announced immediately, interrupting current speech offDefault — updates are not announced
9. Images of Text
Before (Failing)
<!-- Text baked into an image — can't be resized, can't be read by screen readers -->
<img src="summer-sale-banner.jpg" alt="Summer Sale - 50% Off Everything">After (Passing)
<!-- Real text with CSS styling -->
<div class="hero-banner" style="background-image: url('summer-bg.jpg');">
<h1>Summer Sale</h1>
<p class="hero-subtitle">50% Off Everything</p>
</div>
10. Missing Language Attribute
Before (Failing)
<!-- No language specified — screen reader guesses, often incorrectly -->
<html>
<head>
<title>My Website</title>
</head>After (Passing)
<!-- Screen reader uses correct pronunciation rules -->
<html lang="en">
<head>
<title>My Website</title>
</head>
<!-- For content in a different language within the page -->
<p>The French word for hello is <span lang="fr">bonjour</span>.</p>
Remediation Priority Framework
Priority 1: Blockers (Fix Immediately)
Priority 2: Critical (Fix This Sprint)
Priority 3: Important (Fix Next Sprint)
Priority 4: Enhancement (Fix When Convenient)
autocomplete attributes on form fieldsrole="button" on a )
Screen Reader Testing Guide
You’ve reached the end of the free preview
All Chapters
Full Tool Suite
Source Files
Lifetime Updates
📦 Free sample included — download another copy for the full product.