Interaction + Accessibility
When to use: Building any interactive element. Before launch. When you receive accessibility complaints or fail an automated audit.
The test: Unplug your mouse. Close your eyes and turn on a screen reader. Can you still use the page?
Interactive Elements
Every clickable, tappable, or focusable element must be obviously interactive.
Visual Distinction
| Check | Threshold | How to Measure |
|---|---|---|
| Links distinguishable from body text | Underline OR 3:1 contrast difference from surrounding text | Visual inspection + contrast tool |
| Buttons look clickable | Has border (1px+), background color, or shadow -- not styled as plain text | DevTools: verify border, background-color, or box-shadow is set |
| Interactive vs static elements | Zero styled-text elements that look like buttons but aren't | Click everything that looks clickable |
| Consistent patterns | Same element type looks the same everywhere | Visual audit across 3+ pages |
State Design
Every interactive element needs four visible states:
| State | Requirement | How to Verify |
|---|---|---|
| Default | Looks interactive (affordance) | Visual inspection |
| Hover | Visible change within 100ms | Mouse over, watch for change |
| Focus | Visible ring/outline, 3:1 contrast against adjacent colors | Tab to element, verify ring visibility |
| Active/Pressed | Visible change on click/tap | Click and hold, watch for change |
| Disabled | Visually muted, cursor changes, aria-disabled="true" | Inspect element attributes and styles |
State Checklist
- Hover state on all links and buttons (color change, underline, or shadow)
- Focus ring visible on all focusable elements (not suppressed by
outline: none) - Focus ring contrast 3:1 against adjacent background
- Active state provides feedback (scale, color shift, or shadow change)
- Disabled elements have 3:1 contrast against background and
aria-disabled - Transitions between states are 100-200ms (perceptible but not slow)
- No state changes relying on hover alone (mobile has no hover)
Keyboard Navigation
Every action achievable by mouse must be achievable by keyboard.
| Check | Threshold | How to Measure |
|---|---|---|
| All interactive elements reachable | Tab reaches 100% of links, buttons, inputs, controls | Tab through entire page |
| Focus order matches visual order | Tab sequence follows left-to-right, top-to-bottom layout | Tab through, compare to visual layout |
| No keyboard traps | Escape or Tab always moves focus forward | Tab into every component, verify you can Tab out |
| Skip link present | First Tab stop is "Skip to main content" | Tab once on page load |
| Enter/Space activates | Buttons respond to both Enter and Space | Keyboard test on every button |
| Escape closes overlays | Modals, dropdowns, popups close on Escape | Open each, press Escape |
| Arrow keys work in groups | Radio buttons, tabs, menus respond to arrows | Test each grouped component |
Keyboard Checklist
- All links activatable with Enter
- All buttons activatable with Enter and Space
- Custom components (dropdowns, tabs, accordions) follow WAI-ARIA patterns
- Focus returns to trigger element when modal/popup closes
- No
tabindexvalues greater than 0 (breaks natural order) - Focus visible at all times during keyboard navigation
Screen Reader Support
| Check | Threshold | How to Measure |
|---|---|---|
| All images have alt text | 100% of <img> elements | DOM audit: document.querySelectorAll('img:not([alt])') |
| Decorative images hidden | alt="" and aria-hidden="true" on decorative images | Inspect decorative image attributes |
| Heading hierarchy | H1 > H2 > H3, no skipped levels | axe DevTools or manual DOM inspection |
| Landmarks present | <main>, <nav>, <aside>, <footer> used | DOM audit for landmark elements |
| Form labels | Every input has a visible <label> with matching for/id | axe DevTools |
| Error messages | Associated with fields via aria-describedby | Inspect error element attributes |
| Live regions | Dynamic content changes announced via aria-live | Screen reader test on dynamic updates |
Screen Reader Checklist
- Page title describes the page content
- Landmarks used:
main,nav,aside,footer(at minimum) - Headings describe section content (not just styled text)
- Tables have
<th>headers withscopeattributes - Links describe destination (not "click here" or "read more")
- Icons have accessible labels or are hidden from screen readers
- Modal dialogs announce opening with
role="dialog"andaria-labelledby
Color Independence
Information must never rely on color alone.
| Check | Threshold | How to Measure |
|---|---|---|
| Status indicators | Color + icon or text label | Check every status badge, tag, alert |
| Form errors | Red color + error icon + text message | Submit invalid form, check error display |
| Charts and graphs | Color + pattern or label | Enable grayscale emulation in DevTools |
| Links in text | Color + underline or other text decoration | Inspect link styling |
Color Checklist
- Error states use icon + text, not just red color
- Success states use icon + text, not just green color
- Data visualizations readable in grayscale
- Selected/active states use more than color change (bold, border, icon)
- Chrome DevTools > Rendering > Emulate vision deficiency: test all types
Motion and Animation
| Check | Threshold | How to Measure |
|---|---|---|
prefers-reduced-motion respected | All non-essential animation disabled when preference set | Set preference in OS, reload page |
| No auto-playing animation | Zero animations that start without user action (except loading) | Load page, observe |
| Animation duration | Under 400ms for UI transitions | DevTools: inspect transition/animation durations |
| No flashing content | Under 3 flashes per second | Visual observation |
Motion Checklist
-
@media (prefers-reduced-motion: reduce)applied to all animations - Page load animations are optional, not blocking
- Parallax effects disabled for reduced motion preference
- Carousel/slider auto-play has pause control
- No content conveyed only through animation
WCAG 2.2 Additions
WCAG 2.2 added these criteria beyond 2.1:
| Criterion | Requirement | How to Test |
|---|---|---|
| Focus Not Obscured (2.4.11 AA) | Focused element not fully hidden by sticky headers, footers, or overlays | Tab through page with sticky elements |
| Dragging Movements (2.5.7 AA) | Any drag action has a non-drag alternative (click, keyboard) | Try every drag interaction with keyboard |
| Target Size (2.5.8 AA) | Interactive targets 24x24px minimum (44x44px recommended) | DevTools: measure element dimensions |
| Consistent Help (3.2.6 A) | Help mechanisms in same relative location across pages | Check help link/icon position across 3+ pages |
Automated Audit Script
// Run in browser console for quick accessibility check
(function a11yAudit() {
const issues = [];
// Images without alt
document.querySelectorAll("img:not([alt])").forEach((img) => {
issues.push({ type: "missing-alt", element: img, src: img.src });
});
// Inputs without labels
document.querySelectorAll('input:not([type="hidden"]), select, textarea').forEach((input) => {
const id = input.id;
const label = id ? document.querySelector(`label[for="${id}"]`) : null;
const ariaLabel = input.getAttribute("aria-label");
const ariaLabelledBy = input.getAttribute("aria-labelledby");
if (!label && !ariaLabel && !ariaLabelledBy) {
issues.push({ type: "missing-label", element: input });
}
});
// Skipped heading levels
let lastLevel = 0;
document.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((h) => {
const level = parseInt(h.tagName[1]);
if (level > lastLevel + 1 && lastLevel !== 0) {
issues.push({ type: "skipped-heading", element: h, expected: lastLevel + 1, got: level });
}
lastLevel = level;
});
// Buttons/links without accessible names
document.querySelectorAll("button, a[href]").forEach((el) => {
const name = el.textContent.trim() || el.getAttribute("aria-label") || el.getAttribute("title");
if (!name) {
issues.push({ type: "missing-accessible-name", element: el });
}
});
console.table(
issues.map((i) => ({ type: i.type, element: i.element.outerHTML.substring(0, 80) })),
);
console.log(`Found ${issues.length} accessibility issues`);
})();
Quick Diagnosis
| Symptom | Likely Cause | Fix |
|---|---|---|
| Can't tell what's clickable | No hover/focus states | Add state styles to all interactive elements |
| Tab skips elements | Missing from tab order | Check tabindex, use semantic HTML (<button>, <a>) |
| Screen reader says nothing useful | Missing labels and landmarks | Add <label>, aria-label, semantic HTML |
| Keyboard user gets stuck | Focus trap in component | Add Escape handler, ensure Tab exits |
| Color blind users can't see status | Color-only indicators | Add icons or text labels alongside color |
| Page fails axe audit | Multiple WCAG violations | Run axe, fix by severity (critical first) |
Context
- Visual Design -- Contrast and hierarchy standards
- Responsive + Performance -- Touch targets and mobile interaction
- Component Design -- Building accessible components