WCAG 2.2 Accessibility

SkillAI & models

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

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the WCAG 2.2 Accessibility skill

What this skill tells your AI

The instructions your AI receives, as published by claude-dev-suite/claude-dev-suite in skills/accessibility/wcag/SKILL.md and read by ahel’s review.

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

ResourceURL
WCAG 2.2 Spechttps://www.w3.org/TR/WCAG22/
Quick Referencehttps://www.w3.org/WAI/WCAG22/quickref/
Understanding WCAGhttps://www.w3.org/WAI/WCAG22/Understanding/
axe-corehttps://github.com/dequelabs/axe-core
WAI-ARIAhttps://www.w3.org/TR/wai-aria-1.2/

Conformance Levels

LevelDescriptionLegal Requirement
AMinimum accessibilityRarely sufficient
AAStandard accessibilityMost regulations (ADA, EN 301 549)
AAAEnhanced accessibilitySpecialized contexts

WCAG 2.2 Success Criteria Count

LevelNew in 2.2Total
A030
AA624
AAA331

POUR Principles

1. Perceivable

GuidelineKey CriteriaLevel
1.1 Text AlternativesAll non-text content has text alternativeA
1.2 Time-based MediaCaptions, audio descriptionsA-AAA
1.3 AdaptableContent structure, meaningful sequenceA
1.4 DistinguishableColor contrast, resize text, spacingA-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

GuidelineKey CriteriaLevel
2.1 KeyboardAll functionality via keyboardA
2.2 Enough TimeAdjustable time limitsA-AAA
2.4 NavigableSkip links, focus order, focus visibleA-AAA
2.5 Input ModalitiesTarget size, motion alternativesA-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

GuidelineKey CriteriaLevel
3.1 ReadableLanguage of page, unusual wordsA-AAA
3.2 PredictableConsistent navigation, identificationA-AA
3.3 Input AssistanceError identification, labelsA-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

GuidelineKey CriteriaLevel
4.1 CompatibleValid HTML, name/role/valueA
// 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)

CriterionDescription
2.4.11 Focus Not Obscured (Minimum)Focused element at least partially visible
2.4.13 Focus AppearanceFocus indicator meets size/contrast requirements
2.5.7 Dragging MovementsSingle pointer alternative to drag
2.5.8 Target Size (Minimum)24x24 CSS pixels minimum
3.2.6 Consistent HelpHelp in consistent location
3.3.7 Redundant EntryDon't require re-entering info

Level AAA

CriterionDescription
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

ToolPurpose
axe DevToolsBrowser extension for auditing
WAVEVisual accessibility evaluation
LighthousePerformance + accessibility audit
NVDA/VoiceOverScreen reader testing
Color Contrast AnalyzerContrast 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-PatternWhy It's WrongCorrect Approach
Using aria-label on <div> without roleARIA on non-semantic elements without roles is ignoredUse semantic HTML first: <button> instead of <div role="button">
<div onclick=""> for buttonsNot keyboard accessible by defaultUse <button> with proper event handlers
Color-only indicatorsFails for colorblind usersAdd icons, text, or patterns alongside color
placeholder as label replacementDisappears on input, low contrastAlways use <label> with for attribute
tabindex > 0Disrupts natural tab orderUse tabindex="0" or rely on DOM order
alt="" on informative imagesScreen readers skip important contentProvide descriptive alt text
Missing focus indicatorsUsers can't see where they areAlways show :focus-visible styles
Auto-playing mediaDisorienting for screen reader usersRequire user action to start media

Quick Troubleshooting

IssueDiagnosisSolution
Screen reader not announcing buttonMissing accessible nameAdd aria-label or visible text inside button
Keyboard trap in modalFocus not properly managedImplement focus trap with first/last element logic
Form errors not announcedNo role="alert" or aria-liveAdd aria-live="assertive" to error container
Low contrast warningText doesn't meet 4.5:1 ratioUse contrast checker, adjust colors or increase font size
Tab order feels wrongDOM order doesn't match visual orderReorder DOM to match visual layout, avoid tabindex > 0
Custom dropdown not accessibleMissing ARIA roles and keyboard handlingUse role="combobox", aria-expanded, arrow key navigation
Dynamic content not announcedChanges happen silentlyUse aria-live="polite" or role="status"
Image button has no labelOnly icon, no text alternativeAdd aria-label with descriptive text

Related Skills

Signals

GitHub stars
33
Forks
6
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
wcag
Source
github.com/claude-dev-suite/claude-dev-suite