Skip to content
Development
Skill

/accessibility-testing

axe-core integration, WCAG 2.2 AA checklist, keyboard navigation testing, screen reader testing, and ARIA pattern validation.

From plugin
vibecosystem
531200 skills138 agents7 hooks
Install
$ npx -y skills add vibeeval/vibecosystem --skill accessibility-testing --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/accessibility-testing

Context preview

The summary Claude sees to decide when to auto-load this skill.

axe-core integration, WCAG 2.2 AA checklist, keyboard navigation testing, screen reader testing, and ARIA pattern validation.

SKILL.md

accessibility-testing.SKILL.md
name: accessibility-testing
description: axe-core integration, WCAG 2.2 AA checklist, keyboard navigation testing, screen reader testing, and ARIA pattern validation.

Accessibility Testing

axe-core Setup

jest-axe (unit / component tests)

import { axe, toHaveNoViolations } from 'jest-axe'
import { render } from '@testing-library/react'
expect.extend(toHaveNoViolations)

test('LoginForm has no a11y violations', async () => {
  const { container } = render(<LoginForm />)
  const results = await axe(container)
  expect(results).toHaveNoViolations()
})

playwright-axe (e2e)

import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'

test('homepage passes axe audit', async ({ page }) => {
  await page.goto('/')
  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
    .analyze()
  expect(results.violations).toEqual([])
})

cypress-axe

// cypress/support/e2e.js
import 'cypress-axe'

// in test
cy.visit('/')
cy.injectAxe()
cy.checkA11y(null, {
  runOnly: { type: 'tag', values: ['wcag2aa'] },
})

WCAG 2.2 AA Checklist — Top 20 Violations

| # | Criterion | Check | |---|-----------|-------| | 1 | Images have alt text | `<img alt="description">` or `alt=""` for decorative | | 2 | Form inputs have labels | `<label for>` or `aria-label` or `aria-labelledby` | | 3 | Color contrast ≥ 4.5:1 | Normal text; 3:1 for large text (18pt or 14pt bold) | | 4 | Heading hierarchy | h1 → h2 → h3, no skipping levels | | 5 | Keyboard focusable | All interactive elements reachable via Tab | | 6 | Focus visible | `:focus` outline never `outline: none` without replacement | | 7 | No keyboard trap | Tab can always exit modals, dropdowns, widgets | | 8 | Skip navigation link | First focusable element: "Skip to main content" | | 9 | Page has `<title>` | Unique, descriptive per page | | 10 | Language attribute | `<html lang="en">` | | 11 | Error identification | Form errors are text, not color-only | | 12 | Error suggestions | Tell users how to fix the error | | 13 | Link purpose clear | No "click here" or "read more" without context | | 14 | Button text | No icon-only buttons without `aria-label` | | 15 | Table headers | `<th scope="col|row">` on data tables | | 16 | No seizure content | No flashing > 3 times/sec | | 17 | Status messages | `role="status"` or `aria-live` for dynamic updates | | 18 | Reflow at 400% zoom | Single column, no horizontal scroll | | 19 | Text spacing adjustable | No overflow when line-height/letter-spacing increased | | 20 | Timeout warning | Warn before session expires, allow extension |

Keyboard Navigation Test Patterns

Tab order verification

test('modal tab order is correct', async ({ page }) => {
  await page.click('[data-testid="open-modal"]')

  // First focus should be the modal's close button or heading
  await expect(page.locator('[aria-label="Close dialog"]')).toBeFocused()

  // Tab through: close → input → submit
  await page.keyboard.press('Tab')
  await expect(page.locator('#email-input')).toBeFocused()

  await page.keyboard.press('Tab')
  await expect(page.locator('[type="submit"]')).toBeFocused()

  // Wrap back to close button (focus trap)
  await page.keyboard.press('Tab')
  await expect(page.locator('[aria-label="Close dialog"]')).toBeFocused()
})

Focus trap in modals

// Escape should close
await page.keyboard.press('Escape')
await expect(page.locator('[role="dialog"]')).not.toBeVisible()

// Focus returns to trigger element after close
await expect(page.locator('[data-testid="open-modal"]')).toBeFocused()

Skip navigation link

test('skip link goes to main content', async ({ page }) => {
  await page.goto('/')
  await page.keyboard.press('Tab')   // first tab = skip link

  const skipLink = page.locator('a:has-text("Skip to")')
  await expect(skipLink).toBeFocused()

  await page.keyboard.press('Enter')
  await expect(page.locator('main, #main-content')).toBeFocused()
})

Arrow key navigation (listbox/menu)

test('dropdown menu responds to arrow keys', async ({ page }) => {
  await page.click('[aria-haspopup="listbox"]')
  await page.keyboard.press('ArrowDown')  // first option focused
  await page.keyboard.press('ArrowDown')  // second option
  await page.keyboard.press('Enter')      // select

  // Verify selection
  await expect(page.locator('[aria-selected="true"]')).toContainText('Option 2')
})

ARIA Patterns

Live regions (dynamic announcements)

<!-- For important real-time updates (errors, confirmations) -->
<div role="alert">Your payment failed. Please try again.</div>

<!-- For polite updates (search results count) -->
<div aria-live="polite" aria-atomic="true">
  Showing 42 results for "laptop"
</div>

<!-- Screen reader only text (visually hidden) -->
<style>
  .sr-only {
    position: absolute; width: 1px; height: 1px;
    padding: 0; margin: -1px; overflow: hidden;
    clip: rect(0,0,0,0); white-space: nowrap; border: 0;
  }
</style>

Dialog / Modal

<div role="dialog"
     aria-modal="true"
     aria-labelledby="dialog-title"
     aria-describedby="dialog-desc">
  <h2 id="dialog-title">Confirm Delete</h2>
  <p id="dialog-desc">This action cannot be undone.</p>
  <button>Cancel</button>
  <button>Delete</button>
</div>

Tabs

<div role="tablist" aria-label="Account settings">
  <button role="tab" aria-selected="true"  aria-controls="panel-profile" id="tab-profile">Profile</button>
  <button role="tab" aria-selected="false" aria-controls="panel-billing" id="tab-billing" tabindex="-1">Billing</button>
</div>
<div role="tabpanel" id="panel-profile" aria-labelledby="tab-profile">...</div>
<div role="tabpanel" id="panel-billing" aria-labelledby="tab-billing" hidden>...</div>

Combobox / Autocomplete

<label for="search">Search users</label>
<input type="text
Read more
Ships withvibecosystem

Your AI software team. Built on Claude Code. vibecosystem turns Claude Code into a full AI software team — 138 specialized agents that plan, build, review, test, and learn from every mistake. No configuration needed — just install and code.

Get the whole plugin

Other skills on vibecosystem.