accessibility-audit

SKILL.md

Accessibility Audit

Test web applications for WCAG 2.2 compliance and inclusive design.

Automated Scanning

axe-core CLI

# Scan a URL
npx @axe-core/cli https://example.com

# Scan with specific rules
npx @axe-core/cli https://example.com --rules color-contrast,label

# JSON output
npx @axe-core/cli https://example.com --format json > a11y-report.json

# Disable specific rules
npx @axe-core/cli https://example.com --disable scrollable-region-focusable

Lighthouse accessibility audit

# Accessibility category only
lighthouse https://example.com --only-categories=accessibility --output json | jq '{score: .categories.accessibility.score, issues: [.audits | to_entries[] | select(.value.score == 0) | {id: .key, title: .value.title, description: .value.description}]}'

# Desktop
lighthouse https://example.com --only-categories=accessibility --preset=desktop --output json | jq '.categories.accessibility.score'

Playwright with axe-core

# Install
npm install --save-dev @axe-core/playwright

# Test file
cat > a11y.spec.ts << 'EOF'
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('homepage has no a11y violations', async ({ page }) => {
  await page.goto('http://localhost:3000');
  const results = await new AxeBuilder({ page }).analyze();
  expect(results.violations).toEqual([]);
});

test('form page has no a11y violations', async ({ page }) => {
  await page.goto('http://localhost:3000/form');
  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa'])
    .analyze();
  expect(results.violations).toEqual([]);
});
EOF

npx playwright test a11y.spec.ts

Color Contrast

# Check contrast ratio (needs two hex colors)
# WCAG AA: 4.5:1 for normal text, 3:1 for large text
# WCAG AAA: 7:1 for normal text, 4.5:1 for large text

python3 -c "
def luminance(hex_color):
    r, g, b = [int(hex_color[i:i+2], 16)/255 for i in (1, 3, 5)]
    def adjust(c): return c/12.92 if c <= 0.03928 else ((c+0.055)/1.055)**2.4
    return 0.2126*adjust(r) + 0.7152*adjust(g) + 0.0722*adjust(b)

def contrast(c1, c2):
    l1, l2 = luminance(c1), luminance(c2)
    lighter, darker = max(l1,l2), min(l1,l2)
    return (lighter + 0.05) / (darker + 0.05)

fg, bg = '#333333', '#FFFFFF'
ratio = contrast(fg, bg)
print(f'Contrast ratio: {ratio:.2f}:1')
print(f'AA normal text (4.5:1): {\"PASS\" if ratio >= 4.5 else \"FAIL\"}')
print(f'AA large text (3:1): {\"PASS\" if ratio >= 3 else \"FAIL\"}')
print(f'AAA normal text (7:1): {\"PASS\" if ratio >= 7 else \"FAIL\"}')
"

Keyboard Navigation Check

Manual testing checklist

[ ] All interactive elements reachable via Tab
[ ] Tab order follows visual/logical order
[ ] Focus indicator visible on all focused elements
[ ] Escape closes modals/dropdowns
[ ] Enter/Space activates buttons and links
[ ] Arrow keys navigate within menus, tabs, radio groups
[ ] No keyboard traps (can Tab out of every component)
[ ] Skip-to-content link present

Automated focus order check

# Playwright: verify tab order
cat > focus-order.spec.ts << 'EOF'
import { test, expect } from '@playwright/test';

test('tab order is correct', async ({ page }) => {
  await page.goto('http://localhost:3000');
  await page.keyboard.press('Tab');
  const first = await page.evaluate(() => document.activeElement?.tagName);
  expect(first).toBe('A'); // Skip link or first nav item
});
EOF

Common Issues

Issue Fix
Missing alt text on images Add descriptive alt="" (decorative) or alt="description"
Missing form labels Add <label for="id"> or aria-label
Low color contrast Increase contrast to 4.5:1 minimum
Missing heading hierarchy Use h1 → h2 → h3 in order, don't skip levels
Non-descriptive link text "Read more about pricing" not "Click here"
Missing ARIA landmarks Use semantic HTML: <nav>, <main>, <aside>, <footer>
Auto-playing media Add pause/stop controls

Notes

  • Automated tools catch ~30% of accessibility issues. Manual testing is required.
  • Test with actual screen readers: VoiceOver (macOS), NVDA (Windows), TalkBack (Android).
  • Semantic HTML (<button>, <nav>, <main>) is more accessible than <div> with ARIA roles.
  • ARIA is a last resort — use native HTML elements first.
  • Test at 200% zoom — content should reflow without horizontal scrolling.
Weekly Installs
2
First Seen
14 days ago
Installed on
opencode2
gemini-cli2
claude-code2
github-copilot2
codex2
kimi-cli2