Skip to main content

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

CheckThresholdHow to Measure
Links distinguishable from body textUnderline OR 3:1 contrast difference from surrounding textVisual inspection + contrast tool
Buttons look clickableHas border (1px+), background color, or shadow -- not styled as plain textDevTools: verify border, background-color, or box-shadow is set
Interactive vs static elementsZero styled-text elements that look like buttons but aren'tClick everything that looks clickable
Consistent patternsSame element type looks the same everywhereVisual audit across 3+ pages

State Design

Every interactive element needs four visible states:

StateRequirementHow to Verify
DefaultLooks interactive (affordance)Visual inspection
HoverVisible change within 100msMouse over, watch for change
FocusVisible ring/outline, 3:1 contrast against adjacent colorsTab to element, verify ring visibility
Active/PressedVisible change on click/tapClick and hold, watch for change
DisabledVisually 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.

CheckThresholdHow to Measure
All interactive elements reachableTab reaches 100% of links, buttons, inputs, controlsTab through entire page
Focus order matches visual orderTab sequence follows left-to-right, top-to-bottom layoutTab through, compare to visual layout
No keyboard trapsEscape or Tab always moves focus forwardTab into every component, verify you can Tab out
Skip link presentFirst Tab stop is "Skip to main content"Tab once on page load
Enter/Space activatesButtons respond to both Enter and SpaceKeyboard test on every button
Escape closes overlaysModals, dropdowns, popups close on EscapeOpen each, press Escape
Arrow keys work in groupsRadio buttons, tabs, menus respond to arrowsTest 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 tabindex values greater than 0 (breaks natural order)
  • Focus visible at all times during keyboard navigation

Screen Reader Support

CheckThresholdHow to Measure
All images have alt text100% of <img> elementsDOM audit: document.querySelectorAll('img:not([alt])')
Decorative images hiddenalt="" and aria-hidden="true" on decorative imagesInspect decorative image attributes
Heading hierarchyH1 > H2 > H3, no skipped levelsaxe DevTools or manual DOM inspection
Landmarks present<main>, <nav>, <aside>, <footer> usedDOM audit for landmark elements
Form labelsEvery input has a visible <label> with matching for/idaxe DevTools
Error messagesAssociated with fields via aria-describedbyInspect error element attributes
Live regionsDynamic content changes announced via aria-liveScreen 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 with scope attributes
  • 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" and aria-labelledby

Color Independence

Information must never rely on color alone.

CheckThresholdHow to Measure
Status indicatorsColor + icon or text labelCheck every status badge, tag, alert
Form errorsRed color + error icon + text messageSubmit invalid form, check error display
Charts and graphsColor + pattern or labelEnable grayscale emulation in DevTools
Links in textColor + underline or other text decorationInspect 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

CheckThresholdHow to Measure
prefers-reduced-motion respectedAll non-essential animation disabled when preference setSet preference in OS, reload page
No auto-playing animationZero animations that start without user action (except loading)Load page, observe
Animation durationUnder 400ms for UI transitionsDevTools: inspect transition/animation durations
No flashing contentUnder 3 flashes per secondVisual 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:

CriterionRequirementHow to Test
Focus Not Obscured (2.4.11 AA)Focused element not fully hidden by sticky headers, footers, or overlaysTab 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 pagesCheck 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

SymptomLikely CauseFix
Can't tell what's clickableNo hover/focus statesAdd state styles to all interactive elements
Tab skips elementsMissing from tab orderCheck tabindex, use semantic HTML (<button>, <a>)
Screen reader says nothing usefulMissing labels and landmarksAdd <label>, aria-label, semantic HTML
Keyboard user gets stuckFocus trap in componentAdd Escape handler, ensure Tab exits
Color blind users can't see statusColor-only indicatorsAdd icons or text labels alongside color
Page fails axe auditMultiple WCAG violationsRun axe, fix by severity (critical first)

Context