wcag

star 20

WCAG 2.2 Web Content Accessibility Guidelines. Covers conformance levels A/AA/AAA, success criteria, testing with axe-core, and common accessibility patterns. USE WHEN: user mentions "accessibility", "a11y", "WCAG", "screen reader", "ARIA", asks about "color contrast", "keyboard navigation", "accessible forms", "compliance", "ADA", "Section 508" DO NOT USE FOR: automated testing implementation - use `axe-core` instead

claude-dev-suite By claude-dev-suite schedule Updated 2/6/2026

name: wcag description: | WCAG 2.2 Web Content Accessibility Guidelines. Covers conformance levels A/AA/AAA, success criteria, testing with axe-core, and common accessibility patterns.

USE WHEN: user mentions "accessibility", "a11y", "WCAG", "screen reader", "ARIA", asks about "color contrast", "keyboard navigation", "accessible forms", "compliance", "ADA", "Section 508"

DO NOT USE FOR: automated testing implementation - use axe-core instead allowed-tools: Read, Grep, Glob, Write, Edit

WCAG 2.2 Accessibility

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: wcag for comprehensive WCAG guidelines, success criteria, and techniques.

When NOT to Use This Skill

  • Automated testing setup - Use the axe-core skill for integrating axe testing tools
  • Component library accessibility - Use framework-specific skills (e.g., React, Vue) for accessible component patterns
  • Design systems - Use UI library skills for pre-built accessible components
  • ARIA implementation only - This skill covers broader WCAG compliance, not just ARIA

Official References

Resource URL
WCAG 2.2 Spec https://www.w3.org/TR/WCAG22/
Quick Reference https://www.w3.org/WAI/WCAG22/quickref/
Understanding WCAG https://www.w3.org/WAI/WCAG22/Understanding/
axe-core https://github.com/dequelabs/axe-core
WAI-ARIA https://www.w3.org/TR/wai-aria-1.2/

Conformance Levels

Level Description Legal Requirement
A Minimum accessibility Rarely sufficient
AA Standard accessibility Most regulations (ADA, EN 301 549)
AAA Enhanced accessibility Specialized contexts

WCAG 2.2 Success Criteria Count

Level New in 2.2 Total
A 0 30
AA 6 24
AAA 3 31

POUR Principles

1. Perceivable

Guideline Key Criteria Level
1.1 Text Alternatives All non-text content has text alternative A
1.2 Time-based Media Captions, audio descriptions A-AAA
1.3 Adaptable Content structure, meaningful sequence A
1.4 Distinguishable Color contrast, resize text, spacing A-AAA
// Text alternatives
<img src="chart.png" alt="Q3 sales increased 25% compared to Q2" />

// Decorative images
<img src="divider.png" alt="" role="presentation" />

// Color contrast (4.5:1 for normal text, 3:1 for large text)
// Use tools: WebAIM Contrast Checker, axe DevTools

2. Operable

Guideline Key Criteria Level
2.1 Keyboard All functionality via keyboard A
2.2 Enough Time Adjustable time limits A-AAA
2.4 Navigable Skip links, focus order, focus visible A-AAA
2.5 Input Modalities Target size, motion alternatives A-AAA
// Skip link
<a href="#main-content" className="skip-link">
  Skip to main content
</a>

// Focus visible (2.4.7)
button:focus {
  outline: 3px solid #005fcc;
  outline-offset: 2px;
}

// Target size minimum (2.5.8) - 24x24 CSS pixels
.button {
  min-width: 44px;
  min-height: 44px;
  padding: 12px 16px;
}

3. Understandable

Guideline Key Criteria Level
3.1 Readable Language of page, unusual words A-AAA
3.2 Predictable Consistent navigation, identification A-AA
3.3 Input Assistance Error identification, labels A-AAA
// Language of page (3.1.1)
<html lang="en">

// Error identification (3.3.1)
<div role="alert" aria-live="assertive">
  Email is required and must be valid
</div>

// Labels (3.3.2)
<label htmlFor="email">Email Address</label>
<input id="email" type="email" aria-describedby="email-hint" />
<span id="email-hint">We'll never share your email</span>

4. Robust

Guideline Key Criteria Level
4.1 Compatible Valid HTML, name/role/value A
// Name, Role, Value (4.1.2)
<button aria-pressed="true" aria-label="Favorite this item">
  ★
</button>

// Custom controls
<div
  role="slider"
  aria-valuemin={0}
  aria-valuemax={100}
  aria-valuenow={50}
  aria-label="Volume"
  tabIndex={0}
/>

New in WCAG 2.2

Level AA (Required)

Criterion Description
2.4.11 Focus Not Obscured (Minimum) Focused element at least partially visible
2.4.13 Focus Appearance Focus indicator meets size/contrast requirements
2.5.7 Dragging Movements Single pointer alternative to drag
2.5.8 Target Size (Minimum) 24x24 CSS pixels minimum
3.2.6 Consistent Help Help in consistent location
3.3.7 Redundant Entry Don't require re-entering info

Level AAA

Criterion Description
2.4.12 Focus Not Obscured (Enhanced) Focused element fully visible
3.3.8 Accessible Authentication (Minimum) No cognitive function test
3.3.9 Accessible Authentication (Enhanced) No object/content recognition

Common Patterns

Modal Dialog

function Modal({ isOpen, onClose, title, children }) {
  const modalRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (isOpen) {
      // Trap focus inside modal
      modalRef.current?.focus();

      // Prevent body scroll
      document.body.style.overflow = 'hidden';
    }
    return () => {
      document.body.style.overflow = '';
    };
  }, [isOpen]);

  if (!isOpen) return null;

  return (
    <div
      role="dialog"
      aria-modal="true"
      aria-labelledby="modal-title"
      ref={modalRef}
      tabIndex={-1}
      onKeyDown={(e) => e.key === 'Escape' && onClose()}
    >
      <h2 id="modal-title">{title}</h2>
      {children}
      <button onClick={onClose}>Close</button>
    </div>
  );
}

Dropdown Menu

function Dropdown({ label, items }) {
  const [isOpen, setIsOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(-1);

  const handleKeyDown = (e: KeyboardEvent) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        setActiveIndex(i => Math.min(i + 1, items.length - 1));
        break;
      case 'ArrowUp':
        e.preventDefault();
        setActiveIndex(i => Math.max(i - 1, 0));
        break;
      case 'Escape':
        setIsOpen(false);
        break;
      case 'Enter':
      case ' ':
        if (activeIndex >= 0) items[activeIndex].onClick();
        break;
    }
  };

  return (
    <div onKeyDown={handleKeyDown}>
      <button
        aria-haspopup="menu"
        aria-expanded={isOpen}
        onClick={() => setIsOpen(!isOpen)}
      >
        {label}
      </button>
      {isOpen && (
        <ul role="menu">
          {items.map((item, i) => (
            <li
              key={item.id}
              role="menuitem"
              tabIndex={activeIndex === i ? 0 : -1}
              aria-current={activeIndex === i}
            >
              {item.label}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

Form with Validation

function Form() {
  const [errors, setErrors] = useState<Record<string, string>>({});

  return (
    <form aria-describedby="form-errors">
      {Object.keys(errors).length > 0 && (
        <div id="form-errors" role="alert" aria-live="polite">
          <h2>Please fix the following errors:</h2>
          <ul>
            {Object.entries(errors).map(([field, msg]) => (
              <li key={field}>
                <a href={`#${field}`}>{msg}</a>
              </li>
            ))}
          </ul>
        </div>
      )}

      <div>
        <label htmlFor="email">
          Email <span aria-hidden="true">*</span>
          <span className="sr-only">(required)</span>
        </label>
        <input
          id="email"
          type="email"
          aria-required="true"
          aria-invalid={!!errors.email}
          aria-describedby={errors.email ? 'email-error' : undefined}
        />
        {errors.email && (
          <span id="email-error" role="alert">{errors.email}</span>
        )}
      </div>
    </form>
  );
}

Testing

Automated Testing with axe-core

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

test('homepage has no accessibility violations', async ({ page }) => {
  await page.goto('/');

  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
    .analyze();

  expect(results.violations).toEqual([]);
});

// Vitest + axe
import { axe, toHaveNoViolations } from 'jest-axe';
import { render } from '@testing-library/react';

expect.extend(toHaveNoViolations);

test('Button is accessible', async () => {
  const { container } = render(<Button>Click me</Button>);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

Manual Testing Checklist

## Keyboard Navigation
- [ ] Tab through all interactive elements
- [ ] Shift+Tab navigates backwards
- [ ] Enter/Space activates buttons and links
- [ ] Arrow keys work in menus, tabs, sliders
- [ ] Escape closes modals and dropdowns
- [ ] No keyboard traps

## Screen Reader
- [ ] All images have alt text
- [ ] Form fields have labels
- [ ] Headings are hierarchical (h1 > h2 > h3)
- [ ] Links are descriptive (not "click here")
- [ ] Dynamic content announced (aria-live)

## Visual
- [ ] Color contrast meets 4.5:1 (normal text)
- [ ] Color contrast meets 3:1 (large text, UI components)
- [ ] Focus indicators visible
- [ ] Content readable at 200% zoom
- [ ] No horizontal scrolling at 320px width

Tools

Tool Purpose
axe DevTools Browser extension for auditing
WAVE Visual accessibility evaluation
Lighthouse Performance + accessibility audit
NVDA/VoiceOver Screen reader testing
Color Contrast Analyzer Contrast checking

CSS Utilities

/* Visually hidden but accessible to screen readers */
.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;
}

/* Focus visible only for keyboard users */
:focus:not(:focus-visible) {
  outline: none;
}

:focus-visible {
  outline: 3px solid #005fcc;
  outline-offset: 2px;
}

/* Reduced motion preference */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* High contrast mode support */
@media (forced-colors: active) {
  .button {
    border: 2px solid currentColor;
  }
}

Checklist

Design Phase

  • Color contrast meets requirements
  • Touch targets 44x44 minimum (recommended)
  • Focus states designed
  • Error states include text, not just color

Development

  • Semantic HTML used
  • ARIA only when HTML insufficient
  • Keyboard navigation works
  • Focus management for SPAs
  • Form errors accessible

Testing

  • axe-core in CI pipeline
  • Manual screen reader testing
  • Keyboard-only navigation tested
  • Zoom to 200% tested

Anti-Patterns

Anti-Pattern Why It's Wrong Correct Approach
Using aria-label on <div> without role ARIA on non-semantic elements without roles is ignored Use semantic HTML first: <button> instead of <div role="button">
<div onclick=""> for buttons Not keyboard accessible by default Use <button> with proper event handlers
Color-only indicators Fails for colorblind users Add icons, text, or patterns alongside color
placeholder as label replacement Disappears on input, low contrast Always use <label> with for attribute
tabindex > 0 Disrupts natural tab order Use tabindex="0" or rely on DOM order
alt="" on informative images Screen readers skip important content Provide descriptive alt text
Missing focus indicators Users can't see where they are Always show :focus-visible styles
Auto-playing media Disorienting for screen reader users Require user action to start media

Quick Troubleshooting

Issue Diagnosis Solution
Screen reader not announcing button Missing accessible name Add aria-label or visible text inside button
Keyboard trap in modal Focus not properly managed Implement focus trap with first/last element logic
Form errors not announced No role="alert" or aria-live Add aria-live="assertive" to error container
Low contrast warning Text doesn't meet 4.5:1 ratio Use contrast checker, adjust colors or increase font size
Tab order feels wrong DOM order doesn't match visual order Reorder DOM to match visual layout, avoid tabindex > 0
Custom dropdown not accessible Missing ARIA roles and keyboard handling Use role="combobox", aria-expanded, arrow key navigation
Dynamic content not announced Changes happen silently Use aria-live="polite" or role="status"
Image button has no label Only icon, no text alternative Add aria-label with descriptive text

Related Skills

Install via CLI
npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill wcag
Repository Details
star Stars 20
call_split Forks 5
navigation Branch main
article Path SKILL.md
More from Creator
claude-dev-suite
claude-dev-suite Explore all skills →