SuperAPI AI Directory

a11y-debugging

Testing & quality

View repository

Updated 10-15 21:09

About this skill

Core Concepts

Accessibility Tree vs DOM: Visually hiding an element (e.g., CSS opacity: 0) behaves differently for screen readers than display: none or aria-hidden="true". The take_snapshot tool returns the accessibility tree of the page, which represents what assistive technologies "see", making it the most reliable source of truth for semantic structure.

Reading web.dev documentation: If you need to research specific accessibility guidelines (like https://web.dev/articles/accessible-tap-targets), you can append .md.txt to the URL (e.g., https://web.dev/articles/accessible-tap-targets.md.txt) to fetch the clean, raw markdown version. This is much easier to read!

Workflow Patterns

1. Automated Audit (Lighthouse)

Start by running a Lighthouse accessibility audit to get a comprehensive baseline. This tool provides a high-level score and lists specific failing elements with remediation advice.

  1. Run the audit:
    • Set mode to "navigation" to refresh the page and capture load issues.
    • Set outputDirPath (e.g., /tmp/lh-report) to save the full JSON report.
  2. Analyze the Summary:
    • Check scores (0-1 scale). A score < 1 indicates violations.
    • Review audits.failed count.
  3. Review the Report (CRITICAL):
    • Parsing: Do not read the entire file line-by-line. Use a CLI tool like jq or a Node.js one-liner to filter for failures:
      # Extract failing audits with their details
      node -e "const r=require('./report.json'); Object.values(r.audits).filter(a=>a.score!==null && a.score<1).forEach(a=>console.log(JSON.stringify({id:a.id, title:a.title, items:a.details?.items})))"
      
    • This efficiently extracts the selector and snippet of failing elements without loading the full report into context.

2. Browser Issues & Audits

Chrome automatically checks for common accessibility problems. Use list_console_messages to check for these native audits:

  • types: ["issue"]
  • includePreservedMessages: true (to catch issues that occurred during page load)

This often reveals missing labels, invalid ARIA attributes, and other critical errors without manual investigation.

3. Semantics & Structure

The accessibility tree exposes the heading hierarchy and semantic landmarks.

  1. Navigate to the page.
  2. Use take_snapshot to capture the accessibility tree.
  3. Check Heading Levels: Ensure heading levels (h1, h2, h3, etc.) are logical and do not skip levels. The snapshot will include heading roles.
  4. Content Reordering: Verify that the DOM order (which drives the accessibility tree) matches the visual reading order. Use take_screenshot to inspect the visual layout and compare it against the snapshot structure to catch CSS floats or absolute positioning that jumbles the logical flow.

4. Labels, Forms & Text Alternatives

  1. Locate buttons, inputs, and images in the take_snapshot output.
  2. Ensure interactive elements have an accessible name (e.g., a button should not just say "" if it only contains an icon).
  3. Orphaned Inputs: Verify that all form inputs have associated labels. Use evaluate_script to check for inputs missing id (for label[for]) or aria-label:
    () =>
      Array.from(document.querySelectorAll('input, select, textarea'))
        .filter(i => {
          const hasId = i.id && document.querySelector(`label[for="${i.id}"]`);
          const hasAria =
            i.getAttribute('aria-label') || i.getAttribute('aria-labelledby');
          return !hasId && !hasAria && !i.closest('label');
        })
        .map(i => ({
          tag: i.tagName,
          id: i.id,
          name: i.name,
          placeholder: i.placeholder,
        }));
    

4.  Check images for `alt` text.

### 5. Focus & Keyboard Navigation

Testing "keyboard traps" and proper focus management without visual feedback relies on tracking the focused element.

1.  Use the `press_key` tool with `"Tab"` or `"Shift+Tab"` to move focus.
2.  Use `take_snapshot` to capture the updated accessibility tree.
3.  Locate the element marked as focused in the snapshot to verify focus moved to the expected interactive element.
4.  If a modal opens, focus must move into the modal and "trap" within it until closed.

### 6. Tap Targets and Visuals

According to web.dev, tap targets should be at least 48x48 pixels with sufficient spacing. Since the accessibility tree doesn't show sizes, use `evaluate_script`:

```js
// Usage in console: copy, paste, and call with element: fn(element)
el => {
 const rect = el.getBoundingClientRect();
 return {width: rect.width, height: rect.height};
};

Pass the element's uid from the snapshot as an argument to evaluate_script.

7. Color Contrast

To verify color contrast ratios, start by checking for native accessibility issues:

  1. Call list_console_messages with types: ["issue"].
  2. Look for "Low Contrast" issues in the output.

If native audits do not report issues (which may happen in some headless environments) or if you need to check a specific element manually, you can use the following script as a fallback approximation.

Note: This script uses a simplified algorithm and may not account for transparency, gradients, or background images. For production-grade auditing, consider injecting axe-core.

el => {
  function getRGB(colorStr) {
    const match = colorStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
    return match
      ? [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])]
      : [255, 255, 255];
  }
  function luminance(r, g, b) {
    const a = [r, g, b].map(function (v) {
      v /= 255;
      return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
    });
    return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
  }

  const style = window.getComputedStyle(el);
  const fg = getRGB(style.color);
  let bg = getRGB(style.backgroundColor);

  // Basic contrast calculation (Note: Doesn't account for transparency over background images)
  const l1 = luminance(fg[0], fg[1], fg[2]);
  const l2 = luminance(bg[0], bg[1], bg[2]);
  const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);

  return {
    color: style.color,
    bg: style.backgroundColor,
    contrastRatio: ratio.toFixed(2),
  };
};

Pass the element's uid to test the contrast against WCAG AA (4.5:1 for normal text, 3:1 for large text).

8. Global Page Checks

Verify document-level accessibility settings often missed in component testing:

() => ({
  lang:
    document.documentElement.lang ||
    'MISSING - Screen readers need this for pronunciation',
  title: document.title || 'MISSING - Required for context',
  viewport:
    document.querySelector('meta[name="viewport"]')?.content ||
    'MISSING - Check for user-scalable=no (bad practice)',
  reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches
    ? 'Enabled'
    : 'Disabled',
});

Troubleshooting

If standard a11y queries fail or the evaluate_script snippets return unexpected results:

  • Visual Inspection: If automated scripts cannot determine contrast (e.g., text over gradient images or complex backgrounds), use take_screenshot to capture the element. While models cannot measure exact contrast ratios from images, they can visually assess legibility and identifying obvious issues.