DEV Community

Cover image for Web Accessibility Beyond the Checklist: The WCAG Techniques That Actually Matter in Production
Sara Casciaro
Sara Casciaro

Posted on

Web Accessibility Beyond the Checklist: The WCAG Techniques That Actually Matter in Production

Accessibility audits tend to produce the same artifact: a spreadsheet with forty rows, each one a WCAG success criterion, each one marked pass or fail. It's useful for compliance. It's almost useless for knowing what to actually fix first, or understanding why a page that technically passes automated testing still fails a real screen reader user in about eleven seconds.

This guide skips the checklist framing and goes straight to the techniques that produce measurably better outcomes for real assistive technology users, in the order they tend to matter most in production code.

Start With Semantic HTML, Not ARIA

The single highest-leverage accessibility decision happens before any ARIA attribute is written: choosing the right native element. A <button> is focusable, triggers on Enter and Space, is announced as "button" by every screen reader, and works with zero additional code. A <div onClick> styled to look like a button requires tabindex="0", role="button", manual keydown handling for Enter and Space, and still won't behave identically across assistive technologies.

<!-- Fragile: requires manual work to be accessible -->
<div class="btn" onclick="submit()">Submit</div>

<!-- Correct: accessible by default -->
<button type="button" onclick="submit()">Submit</button>
Enter fullscreen mode Exit fullscreen mode

This applies across the board. <nav> instead of a <div> with a role. <main> for primary content. <a href> for navigation, <button> for actions — this distinction alone resolves a huge share of real-world screen reader confusion, because screen reader users frequently navigate by element type: "list all links on this page," "list all buttons." An anchor tag used as a button (or vice versa) breaks that navigation pattern even if it looks and behaves correctly visually.

The rule that holds up: reach for ARIA only when no native HTML element or attribute provides the semantics you need. The first rule of ARIA use, as the W3C's own authoring practices state directly, is not to use ARIA if a native HTML element or attribute already has the semantics you require.

Focus Management: The Part Everyone Skips

Visual design rarely accounts for keyboard focus, which means focus management is usually the least tested part of an interface and the most consequential for keyboard-only users.

Focus must be visible. Removing the default focus outline without replacing it (outline: none with nothing to substitute) is one of the most common accessibility regressions in production CSS, usually introduced because the default outline "didn't match the design." A visible custom focus state costs nothing functionally and is non-negotiable:

/* Never do this alone */
button:focus {
  outline: none;
}

/* Do this instead */
button:focus-visible {
  outline: 2px solid var(--color-focus-ring);
  outline-offset: 2px;
}
Enter fullscreen mode Exit fullscreen mode

:focus-visible (not :focus) is the modern approach: it shows the focus ring for keyboard navigation while suppressing it for mouse clicks, matching what users actually expect without extra JavaScript.

Focus must move logically when the DOM changes. This is where most single-page interfaces fail silently. When a modal opens, focus needs to move into it. When it closes, focus needs to return to the element that opened it. When a route changes in a client-rendered app, focus needs to move to the new page's heading, or a screen reader user has no signal that anything happened at all.

function openModal(modalElement, triggerElement) {
  const previouslyFocused = triggerElement;
  const firstFocusable = modalElement.querySelector(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  firstFocusable?.focus();

  function trapFocus(e) {
    if (e.key !== 'Tab') return;
    const focusables = modalElement.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    const first = focusables[0];
    const last = focusables[focusables.length - 1];

    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault();
      last.focus();
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault();
      first.focus();
    }
  }

  modalElement.addEventListener('keydown', trapFocus);

  return function closeModal() {
    modalElement.removeEventListener('keydown', trapFocus);
    previouslyFocused?.focus();
  };
}
Enter fullscreen mode Exit fullscreen mode

Focus must never land on something invisible. A common bug: a form validation error appears, but the focus doesn't move to it, so keyboard and screen reader users have no idea an error exists until they tab past it eventually. Move focus to the first error on failed validation, and announce it.

Color Contrast: The Math You Should Actually Run

"Looks readable to me" is not a contrast check. WCAG 2.1 defines precise ratios: 4.5:1 for normal text at AA level, 3:1 for large text (18pt+/24px+, or 14pt/18.5px+ bold), and 7:1 / 4.5:1 respectively for the stricter AAA level.

The contrast ratio formula compares relative luminance of foreground and background:

ratio = (L1 + 0.05) / (L2 + 0.05)
Enter fullscreen mode Exit fullscreen mode

where L1 is the relative luminance of the lighter color and L2 the darker one. You don't need to compute this by hand — WebAIM's contrast checker and the Chrome DevTools color picker both do it live — but understanding that it's a luminance ratio, not a subjective "does it look okay," changes how you evaluate brand colors against backgrounds.

The practical failure mode worth watching for: light gray text (#999999 or lighter) on a white background is a near-universal design trend that frequently fails AA at typical body text sizes. #767676 is roughly the lightest gray that passes 4.5:1 against pure white — anything lighter needs a larger font size or a darker shade to be compliant.

/* Fails AA at 16px: contrast ratio ~2.85:1 */
.muted-text {
  color: #AAAAAA;
}

/* Passes AA at 16px: contrast ratio ~4.54:1 */
.muted-text {
  color: #767676;
}
Enter fullscreen mode Exit fullscreen mode

Keyboard Testing Is the Fastest Real Signal You Have

Before running any automated tool, unplug your mouse and try to complete your site's primary user flow — sign up, add to cart, submit a form — using only Tab, Shift+Tab, Enter, Space, and arrow keys where relevant.

This single test surfaces more real issues than most automated scanners, because automated tools (axe, Lighthouse accessibility audit, WAVE) catch roughly 30-40% of WCAG issues by design — they can verify the presence of an alt attribute, not whether its content is meaningful, and they can verify a focus outline exists, not whether the tab order makes logical sense.

Things to watch for during manual keyboard testing: elements that receive focus but shouldn't (decorative icons with a stray tabindex="0"), elements that should receive focus but don't (custom dropdowns built from <div> elements), a tab order that jumps illogically around the visual layout, and keyboard traps — places where focus enters but Tab and Shift+Tab can't get it back out, which is a hard WCAG failure (2.1.2) and traps exactly the users least able to work around it with a mouse click.

Forms: Where Accessibility and Usability Are the Same Problem

Form accessibility issues are disproportionately damaging because forms are where conversion happens — accessibility failures here don't just violate a guideline, they directly block revenue.

Every input needs a programmatically associated label, not just visual proximity:

<!-- Visually fine, programmatically disconnected -->
<span>Email</span>
<input type="email" />

<!-- Programmatically associated -->
<label for="email">Email</label>
<input type="email" id="email" />
Enter fullscreen mode Exit fullscreen mode

Error messages need to be associated with their field via aria-describedby, and the field itself needs aria-invalid="true" when validation fails, so a screen reader announces both the error state and the error text when the user reaches or re-focuses the field:

<label for="email">Email</label>
<input
  type="email"
  id="email"
  aria-invalid="true"
  aria-describedby="email-error"
/>
<span id="email-error" role="alert">
  Please enter a valid email address.
</span>
Enter fullscreen mode Exit fullscreen mode

role="alert" on the error message means it gets announced immediately when it appears in the DOM, without requiring the user to have focus on it — important for errors that appear after a delayed async validation.

Reduced Motion Is Not a Nice-to-Have

For users with vestibular disorders, animation isn't a stylistic preference issue — parallax scrolling, large-scale motion, and auto-playing transitions can trigger genuine physical symptoms: nausea, dizziness, disorientation. The prefers-reduced-motion media query exists specifically for this, and respecting it is a two-line CSS addition with no functional cost:

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
Enter fullscreen mode Exit fullscreen mode

This is one of the rare accessibility techniques that costs the implementing developer nothing and directly prevents physical harm to a defined group of users. There's no good reason to skip it.

Where to Actually Start

If you're looking at an existing codebase with no accessibility work done, the order of operations that produces the fastest improvement in real usability, not just audit scores, is: fix focus visibility globally first (one CSS rule, site-wide impact), then audit form labels and error handling (highest business impact per fix), then run a full keyboard-only pass on your primary conversion flow, then address color contrast on body text and interactive elements, then move to ARIA and dynamic content announcements last, because that's where mistakes are easiest to make and hardest to test without an actual screen reader.

Automated tools are a floor, not a ceiling. They will tell you what's definitely broken. They will not tell you what's merely technically compliant and still unusable. The keyboard-only pass and, ideally, five minutes with an actual screen reader (VoiceOver on Mac, NVDA free on Windows) will tell you the rest.


Written by Sara Casciaro, founder of Sabriel Agency, digital studio in Ugento (LE), Italy.

Top comments (0)