Frontend12 min read

Debugging Accessibility Violations: A Systematic Approach from Audit to Fix

A practical guide to identifying, triaging, and fixing accessibility violations in web applications, covering automated tools, manual testing techniques, and common pitfalls.

accessibilitya11ydebuggingWCAGARIA

I've been on both sides of the accessibility fence: the developer who ships a feature with a missing aria-label, and the one who gets a bug report from a user who can't use the app at all. Debugging accessibility violations isn't just about running a tool and fixing the red lines. It's about understanding what each violation means for a real person, then methodically working through the stack to resolve it.

In this article, I'll walk through a systematic approach to debugging accessibility issues, from initial automated scans to manual validation with assistive technology. I'll include a specific war story from a recent project where a missing focus trap caused a cascade of violations.

Step 1: Automated Auditing – Finding the Low-Hanging Fruit

Always start with an automated scan. Tools like axe-core (the engine behind Deque's axe DevTools) and Lighthouse catch around 30% of all violations. They're great for catching missing alt text, insufficient color contrast, and missing form labels.

I integrate axe-core into my CI pipeline. Here's a minimal example using Puppeteer to run an audit on every pull request:

Automated accessibility audit with axe-core and Puppeteer
const { AxePuppeteer } = require('@axe-core/puppeteer');
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');

  const results = await new AxePuppeteer(page).analyze();
  console.log(`Violations: ${results.violations.length}`);
  results.violations.forEach(v => {
    console.log(`${v.id}: ${v.description}`);
  });

  await browser.close();
})();

But automated tools have blind spots. They can't judge whether an aria-label is meaningful or if a custom widget's keyboard interaction feels natural. That's where manual testing comes in.

Color Contrast: The Most Common Violation

In my experience, color contrast violations are the single most common issue. They're also the easiest to fix programmatically. The WCAG AA standard requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text. Use a tool like axe-core or the Chrome DevTools contrast checker to identify problem areas.

One gotcha: gradient backgrounds can reduce contrast in unexpected ways. Always test the worst-case point in the gradient.

Step 2: Manual Keyboard Navigation – The Real Test

After fixing the automated violations, I do a full keyboard walkthrough. I close my laptop lid (so the touchpad is disabled) and navigate the entire app using only Tab, Shift+Tab, Enter, Escape, and arrow keys. This reveals issues that no tool can catch: invisible focus indicators, broken tab order, and elements that trap focus.

Here's a specific example from a recent project: we had a modal dialog that, when opened, did not trap focus. Users could tab out of the modal and interact with the background page, causing confusion and potential data loss.

The Focus Trap That Wasn't

  1. 10:00QA reports that after opening a modal, pressing Tab moves focus to the browser URL bar instead of cycling within the modal.
  2. 10:15I reproduce: open modal, press Tab → focus leaves modal. The modal has no focus trap logic.
  3. 10:30I add a focus trap: I store a reference to the modal container, then on Tab/Shift+Tab, I manually cycle focus between the first and last focusable elements inside the modal.
  4. 10:45Re-test: Tab now stays inside the modal. Pushed fix.

Lesson

Always implement a focus trap for modals, drawers, and any overlay that blocks the main content. Use a library like focus-trap-react if you're in React, or write a simple function for vanilla JS.

The fix was straightforward: I used a focus-trap utility that listens for Tab and Shift+Tab and redirects focus to the first or last focusable element when the user tries to leave the modal.

A simple focus trap function for modal dialogs
// Minimal focus trap for a modal
function trapFocus(container) {
  const focusable = container.querySelectorAll(
    'a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])'
  );
  const first = focusable[0];
  const last = focusable[focusable.length - 1];

  container.addEventListener('keydown', (e) => {
    if (e.key !== 'Tab') return;
    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault();
      last.focus();
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault();
      first.focus();
    }
  });
}

Step 3: Screen Reader Testing – Hearing the Experience

Keyboard navigation works, but does the app make sense when read aloud? I test with VoiceOver (macOS) and NVDA (Windows). A common issue is missing or incorrect ARIA roles and states.

For example, a custom dropdown might read as 'button' but not announce its expanded state. Adding aria-expanded and role='combobox' fixes that.

warning

Never use ARIA if a native HTML element suffices. Native elements have built-in keyboard handling, focus management, and screen reader announcements. Using a <button> is always better than a <div> with role='button'.

70%

of accessibility violations are missed by automated tools and require manual testing

ARIA Misuse: A Common Pitfall

I once inherited a codebase where every interactive element had role='button', even links and headings. The result: screen readers would announce everything as 'button', making the app unusable. The fix was to remove all ARIA roles and states and replace them with proper HTML elements. Then add ARIA only where necessary, like for custom widgets.

Always validate your ARIA with the WAI-ARIA spec. Tools like axe-core can detect some misuse, but manual review is better.

  1. 1Audit with axe-core or Lighthouse, fix all violations.
  2. 2Test keyboard navigation: Tab through every interactive element, check focus visibility and order.
  3. 3Test with a screen reader: navigate the page, listen for meaningful announcements.
  4. 4Verify with users if possible – nothing beats real feedback.

Conclusion

Debugging accessibility violations is a systematic process: automate what you can, then manually verify what automation misses. Focus on the most impactful fixes first – color contrast and keyboard navigation – then move to screen reader compatibility.

The war story I shared is a reminder that even a small oversight (like a missing focus trap) can break the experience for users who rely on keyboards. Every fix you make is a step toward a more inclusive web.

Frequently asked questions

What is the difference between automated and manual accessibility testing?

Automated testing (e.g., axe-core, Lighthouse) catches about 30% of all violations, mostly programmatic ones like missing alt text or insufficient color contrast. Manual testing covers the remaining 70%, including keyboard navigation, screen reader announcements, and cognitive load issues. Both are essential.

How do I fix an ARIA attribute that is misused?

First, determine if the ARIA is even necessary. If a native HTML element (like <button> or <input>) can do the job, use that instead. If you must use ARIA, ensure roles, properties, and states are applied according to the WAI-ARIA spec. For example, add role='tab' to tab elements and manage aria-selected dynamically.

Why is keyboard navigation important for accessibility?

Many users rely on keyboards instead of a mouse, including those with motor disabilities and power users. If a feature is not keyboard-accessible, it's completely unusable for them. Common issues include missing focus indicators, non-standard tab order, and elements that can't be activated with Enter or Space.

What is the most common accessibility violation I should fix first?

Color contrast violations are the most common – they affect users with low vision and are easy to fix by adjusting text and background colors to meet WCAG AA ratios (4.5:1 for normal text, 3:1 for large text). Use tools like Contrast Checker or axe-core to identify them.