Contents

Chapter 1

Keyboard Navigation Test Matrix

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.


How to Use This Matrix

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

Testing Setup

  • Browser: Test in Chrome, Firefox, and Safari (keyboard behavior varies)
  • Disable your mouse (unplug it or put it out of reach) to force keyboard-only navigation
  • Focus indicator: If you can't see where focus is, that's already a failure (WCAG 2.4.7)
  • Tab order tool: Use the browser's Accessibility tab in DevTools to inspect focus order

These apply to every page, regardless of what components are present.

TestKeysExpected BehaviorPass/Fail
Tab through all elementsTabFocus moves forward through all interactive elements in visual reading order
Reverse tabShift+TabFocus moves backward through interactive elements
Skip navigationTab (first press)First focusable element is a "Skip to content" link
Skip link worksEnter on skip linkFocus moves to main content area
No keyboard trapsTab repeatedlyFocus eventually returns to the browser chrome (URL bar); never gets stuck
Focus visibleTab through allEvery focused element shows a visible focus indicator (outline, ring, highlight)
Focus order logicalTab through allFocus order matches visual reading order (left-to-right, top-to-bottom in LTR)
No focus jumpsInteract with elementsFocus doesn't jump to unexpected locations after interactions

TestKeysExpected BehaviorPass/Fail
Focus on linkTabLink receives visible focus indicator
Activate linkEnterLink activates (navigates to destination or triggers action)
Link textN/A (inspect)Link text describes the destination, not "click here" or "read more"

Buttons

TestKeysExpected BehaviorPass/Fail
Focus on buttonTabButton receives visible focus indicator
Activate buttonEnterButton activates its action
Activate buttonSpaceButton activates its action (both Enter and Space should work)
Toggle buttonSpaceToggles state and announces new state (aria-pressed changes)
Disabled buttonTabDisabled buttons should either be skipped in tab order OR be focusable but inoperable

Text Inputs

TestKeysExpected BehaviorPass/Fail
Focus on inputTabInput receives focus; label is announced by screen reader
Type in inputCharacter keysCharacters appear in the input field
Select all textCtrl+AAll text in the field is selected
Copy/pasteCtrl+C / Ctrl+VStandard clipboard operations work
Move focus awayTab / Shift+TabFocus leaves the input without submitting the form
Required fieldTab away emptyError message appears and is associated with the field

Checkboxes

TestKeysExpected BehaviorPass/Fail
Focus on checkboxTabCheckbox receives visible focus indicator
Toggle checkboxSpaceCheckbox toggles between checked and unchecked
Group navigationTabTab moves between checkbox groups (not between individual checkboxes in a group)
Indeterminate stateSpaceIf indeterminate, toggles to checked or unchecked

Radio Buttons

TestKeysExpected BehaviorPass/Fail
Focus into groupTabFocus enters the radio group on the selected option (or first if none selected)
Move within groupArrow Up/DownFocus and selection move between options
Move within groupArrow Left/RightSame as Up/Down (horizontal radio groups)
Wrap aroundArrow Down on lastFocus wraps to the first option
Leave groupTabFocus leaves the radio group entirely (moves to next component)

Select / Dropdown (Native)

TestKeysExpected BehaviorPass/Fail
Focus on selectTabSelect element receives visible focus
Open dropdownSpace / Enter / Alt+DownDropdown opens showing options
Navigate optionsArrow Up/DownMoves through options
Select optionEnterSelects the highlighted option and closes the dropdown
Close without selectingEscapeDropdown closes; previous selection retained
Type-aheadCharacter keyJumps to the first option starting with that character

Custom Dropdown / Listbox (ARIA)

TestKeysExpected BehaviorPass/Fail
Focus on triggerTabTrigger button/input receives visible focus
Open listboxEnter / Space / Arrow DownListbox appears; first option (or previously selected) is highlighted
Navigate optionsArrow Up/DownHighlight moves through options
Select optionEnterOption is selected; listbox closes; trigger announces selection
Close listboxEscapeListbox closes; focus returns to trigger; no selection change
First/last optionHome / EndHighlight jumps to first/last option
Type-aheadCharacter keyHighlight moves to next option starting with that character
aria-activedescendantArrow keysaria-activedescendant on the input updates to the highlighted option's ID

Tabs

TestKeysExpected BehaviorPass/Fail
Focus into tabsTabFocus lands on the active tab (not the first tab if another is selected)
Switch tabsArrow Left/RightFocus and activation move between tabs
First tabHomeFocus and activation move to the first tab
Last tabEndFocus and activation move to the last tab
Wrap aroundArrow Right on lastFocus wraps to the first tab
Enter panelTab (from tab)Focus moves into the active tab panel content
Leave tabsShift+Tab (from panel)Focus returns to the tab list
Panel visibilityArrow keysOnly the active panel is visible; others are hidden or display: none

TestKeysExpected BehaviorPass/Fail
Open modalEnter/Space on triggerModal opens; focus moves to the first focusable element inside (or the close button)
Close modalEscapeModal closes; focus returns to the trigger element
Focus trap (forward)Tab on last elementFocus wraps to the first focusable element in the modal
Focus trap (backward)Shift+Tab on first elementFocus wraps to the last focusable element in the modal
Background inertTab repeatedlyFocus never leaves the modal while it's open
Close via buttonEnter/Space on close buttonModal closes; focus returns to trigger
Background clickClick backdropModal closes (optional but common); focus returns to trigger
Scroll lockArrow keys / scrollBackground page doesn't scroll while modal is open

Disclosure / Accordion

TestKeysExpected BehaviorPass/Fail
Focus on triggerTabDisclosure trigger (button) receives visible focus
Toggle openEnter or SpaceContent is revealed; aria-expanded changes to "true"
Toggle closedEnter or Space againContent is hidden; aria-expanded changes to "false"
Navigate to contentTabFocus moves into the revealed content (if it contains focusable elements)
Accordion: single openEnter on closed itemThe clicked item opens; other items close

TestKeysExpected BehaviorPass/Fail
Focus on buttonTabMenu button receives visible focus
Open menuEnter / Space / Arrow DownMenu opens; focus moves to the first menu item
Open at endArrow UpMenu opens; focus moves to the last menu item
Navigate itemsArrow Up/DownFocus moves through menu items
Activate itemEnterItem action is triggered; menu closes; focus returns to trigger
Close menuEscapeMenu closes; focus returns to the trigger button
Close and moveTabMenu closes; focus moves to the next element after the menu button
First/last itemHome / EndFocus moves to first/last menu item
Type-aheadCharacter keyFocus moves to the next item starting with that character
Disabled itemsArrow DownFocus skips disabled items

Tooltip

TestKeysExpected BehaviorPass/Fail
Show tooltipFocus (Tab to element)Tooltip appears when the trigger receives focus
Hide tooltipEscapeTooltip hides (WCAG 1.4.13 — dismissable)
Hide on blurTab awayTooltip hides when focus leaves the trigger
Not hoverable requiredN/ATooltip content is available without needing to hover over it

Slider / Range

TestKeysExpected BehaviorPass/Fail
Focus on sliderTabSlider thumb receives visible focus
IncrementArrow Right / Arrow UpValue increases by one step
DecrementArrow Left / Arrow DownValue decreases by one step
Large incrementPage UpValue increases by a larger step (typically 10%)
Large decrementPage DownValue decreases by a larger step
Set to minimumHomeValue jumps to minimum
Set to maximumEndValue jumps to maximum
Value announcedAny changearia-valuenow, aria-valuetext are updated and announced

Drag and Drop

TestKeysExpected BehaviorPass/Fail
Keyboard alternative existsN/ADrag-and-drop has a keyboard-accessible alternative (move up/down buttons, select+move)
Reorder with buttonsEnter on move buttonItem moves in the specified direction
Announce new positionAfter moveScreen reader announces the item's new position ("Item 3 of 5")

TestKeysExpected BehaviorPass/Fail
Previous/Next buttonsTab + EnterPrevious and Next buttons are keyboard accessible
Slide indicatorsTab + EnterIndividual slide indicators (dots) are keyboard accessible
Pause auto-playEnter on pause buttonAuto-rotating carousel can be paused
Swipe alternativeArrow Left/Right or buttonsTouch swipe has a keyboard/button alternative

Data Table (Sortable)

TestKeysExpected BehaviorPass/Fail
Focus on sort buttonTabColumn header sort button receives focus
Activate sortEnter or SpaceTable re-sorts by that column; sort direction announced
Navigate cellsArrow keys (with screen reader)Screen reader announces row/column headers as you move between cells

Summary Template

markdown
# 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

Chapter 2

Accessibility Remediation Guide

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.


1. Missing or Poor Alternative Text

WCAG: 1.1.1 Non-text Content (Level A)

Frequency: Found on nearly every site audited

Problem

Images without alt attributes, or with unhelpful alt text like alt="image" or alt="IMG_4521.jpg".

Before (Failing)

html
<!-- 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">

After (Passing)

html
<!-- 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 -->

Decision Tree for Alt Text

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

2. Missing Form Labels

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

Problem

Form inputs without elements, relying on placeholder text instead.

Before (Failing)

html
<!-- 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">

After (Passing)

html
<!-- 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">

Priority Order

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)


3. Insufficient Color Contrast

WCAG: 1.4.3 Contrast Minimum (Level AA)

Frequency: Found on most sites, especially with light gray text

Problem

Text colors that don't have enough contrast against their background.

Minimum Ratios

Text SizeMinimum Contrast Ratio
Normal text (< 18px, or < 14px bold)4.5:1
Large text (≥ 18px, or ≥ 14px bold)3:1
UI components and graphics3:1

Common Failures

css
/* 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; }

Fixes

css
/* 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; }

Tools


4. Missing Heading Structure

WCAG: 1.3.1 Info and Relationships (Level A), 2.4.6 Headings and Labels (Level AA)

Frequency: Very common — styled

and elements instead of real headings

Before (Failing)

html
<!-- 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)

html
<!-- 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

WCAG: 2.1.1 Keyboard (Level A)

Frequency: Common with custom JavaScript components

Before (Failing)

html
<!-- 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)

html
<!-- 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

Use native HTML elements whenever possible.