---
name: accessibility-compliance
description: >-
  Implement WCAG 2.2 compliant interfaces, conduct accessibility audits, and remediate violations with automated testing and inclusive design patterns. Use when auditing accessibility, fixing WCAG violations, implementing ARIA patterns, building for screen readers, ensuring inclusive UX, or when user mentions "WCAG audit", "accessibility audit", "WCAG violations", "remediation", "Section 508", "ADA compliance", "VPAT", "screen reader support", or "assistive technology".
enabled: false
source: github:JuanJoseGonGi/skills
imported-from: github:JuanJoseGonGi/skills
---

# Accessibility Compliance & Audit

Implement accessible interfaces, audit against WCAG 2.2, and remediate violations — covering automated testing, manual verification, ARIA patterns, and inclusive design.

## When to Use This Skill

- Implementing WCAG 2.2 Level AA or AAA compliance
- Conducting accessibility audits or fixing violations
- Preparing for ADA/Section 508 requirements or VPAT compliance
- Building screen reader accessible interfaces
- Adding keyboard navigation to interactive components
- Implementing focus management and focus trapping
- Creating accessible forms with proper labeling
- Supporting reduced motion and high contrast preferences
- Building mobile accessibility (iOS VoiceOver, Android TalkBack)

## WCAG Foundations

### Conformance Levels

| Level   | Description            | Required For      |
| ------- | ---------------------- | ----------------- |
| **A**   | Minimum accessibility  | Legal baseline    |
| **AA**  | Standard conformance   | Most regulations  |
| **AAA** | Enhanced accessibility | Specialized needs |

### POUR Principles

- **Perceivable**: Content presentable in different ways (alt text, captions, contrast)
- **Operable**: Navigable with keyboard and assistive tech (no traps, enough time)
- **Understandable**: Content and operation are clear (labels, errors, consistency)
- **Robust**: Works with current and future assistive technologies (valid markup, ARIA)

### Common Violations by Impact

```
Critical (Blockers):
├── Missing alt text for functional images
├── No keyboard access to interactive elements
├── Missing form labels
└── Auto-playing media without controls

Serious:
├── Insufficient color contrast
├── Missing skip links
├── Inaccessible custom widgets
└── Missing page titles

Moderate:
├── Missing language attribute
├── Unclear link text ("click here")
├── Missing landmarks
└── Improper heading hierarchy
```

> Full per-criterion audit checklist: see `references/wcag-audit-checklist.md`

### Key Success Criteria

| Level | Criterion | Description                                  |
| ----- | --------- | -------------------------------------------- |
| A     | 1.1.1     | Non-text content has text alternatives       |
| A     | 1.3.1     | Info and relationships determinable          |
| A     | 2.1.1     | All functionality keyboard accessible        |
| A     | 2.4.1     | Skip to main content mechanism               |
| AA    | 1.4.3     | Contrast ratio 4.5:1 text, 3:1 large text   |
| AA    | 1.4.11    | Non-text contrast 3:1                        |
| AA    | 2.4.7     | Focus visible                                |
| AA    | 2.5.8     | Target size minimum 24x24px (NEW in 2.2)     |
| AAA   | 1.4.6     | Enhanced contrast 7:1                        |
| AAA   | 2.5.5     | Target size minimum 44x44px                  |

## ARIA Patterns

- **Roles**: Define element purpose (button, dialog, navigation)
- **States**: Indicate current condition (expanded, selected, disabled)
- **Properties**: Describe relationships (labelledby, describedby)
- **Live regions**: Announce dynamic content changes

> Detailed ARIA reference: see `references/aria-patterns.md`

## Implementation Patterns

### Accessible Button

```tsx
function AccessibleButton({ children, isLoading, disabled, ...props }: ButtonProps) {
  return (
    <button
      disabled={disabled || isLoading}
      aria-busy={isLoading}
      aria-disabled={disabled || isLoading}
      className={cn(
        "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
        "min-h-[44px] min-w-[44px]", // Touch target size
        (disabled || isLoading) && "opacity-50 cursor-not-allowed"
      )}
      {...props}
    >
      {isLoading ? (
        <>
          <span className="sr-only">Loading</span>
          <Spinner aria-hidden="true" />
        </>
      ) : children}
    </button>
  );
}
```

### Accessible Modal Dialog

```tsx
function AccessibleDialog({ isOpen, onClose, title, children }: DialogProps) {
  const titleId = React.useId();

  React.useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === "Escape" && isOpen) onClose();
    };
    document.addEventListener("keydown", handleKeyDown);
    return () => document.removeEventListener("keydown", handleKeyDown);
  }, [isOpen, onClose]);

  React.useEffect(() => {
    if (isOpen) document.body.style.overflow = "hidden";
    return () => { document.body.style.overflow = ""; };
  }, [isOpen]);

  if (!isOpen) return null;

  return (
    <div role="dialog" aria-modal="true" aria-labelledby={titleId}>
      <div className="fixed inset-0 bg-black/50" aria-hidden="true" onClick={onClose} />
      <FocusTrap>
        <div className="fixed inset-0 flex items-center justify-center p-4">
          <div className="bg-background rounded-lg shadow-lg max-w-md w-full p-6">
            <h2 id={titleId} className="text-lg font-semibold">{title}</h2>
            {children}
            <button onClick={onClose} className="absolute top-4 right-4" aria-label="Close dialog">
              <X className="h-4 w-4" />
            </button>
          </div>
        </div>
      </FocusTrap>
    </div>
  );
}
```

### Accessible Form with Error Handling

```tsx
function AccessibleForm() {
  const [errors, setErrors] = React.useState<Record<string, string>>({});

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

      <label htmlFor="email">
        Email address
        <span aria-hidden="true" className="text-destructive ml-1">*</span>
        <span className="sr-only">(required)</span>
      </label>
      <input
        id="email" type="email" required
        aria-required="true"
        aria-invalid={!!errors.email}
        aria-describedby={errors.email ? "email-error" : "email-hint"}
      />
      {errors.email
        ? <p id="email-error" role="alert">{errors.email}</p>
        : <p id="email-hint">We'll never share your email.</p>}
    </form>
  );
}
```

### Skip Navigation & Landmarks

```tsx
function Layout({ children }) {
  return (
    <>
      <a href="#main-content" className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50">
        Skip to main content
      </a>
      <header>...</header>
      <nav aria-label="Main navigation">...</nav>
      <main id="main-content" tabIndex={-1}>{children}</main>
      <footer>...</footer>
    </>
  );
}
```

### Live Region Announcements

```tsx
function useAnnounce() {
  const [message, setMessage] = React.useState("");
  const announce = React.useCallback((text: string) => {
    setMessage("");
    setTimeout(() => setMessage(text), 100);
  }, []);
  const Announcer = () => (
    <div role="status" aria-live="polite" aria-atomic="true" className="sr-only">{message}</div>
  );
  return { announce, Announcer };
}
```

## Automated Testing

### axe-core Integration

```javascript
async function runAccessibilityAudit(page) {
  await page.addScriptTag({ path: require.resolve('axe-core') });
  const results = await page.evaluate(async () => {
    return await axe.run(document, {
      runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa'] }
    });
  });
  return { violations: results.violations, passes: results.passes, incomplete: results.incomplete };
}

// Playwright test
test('should have no accessibility violations', async ({ page }) => {
  await page.goto('/');
  const results = await runAccessibilityAudit(page);
  expect(results.violations).toHaveLength(0);
});
```

### CLI Tools

```bash
npx @axe-core/cli https://example.com
npx pa11y https://example.com
lighthouse https://example.com --only-categories=accessibility
```

> Automated testing catches 30–50% of issues. Manual testing is always required.

## Remediation Patterns

### Fix: Missing Form Labels

```html
<!-- Before -->
<input type="email" placeholder="Email" />

<!-- After: visible label (preferred) -->
<label for="email">Email address</label>
<input id="email" type="email" />

<!-- After: aria-label (when visual label not possible) -->
<input type="email" aria-label="Email address" />
```

### Fix: Insufficient Color Contrast

```css
/* Before: 2.5:1 — fails AA */
.text { color: #767676; }

/* After: 4.5:1 — passes AA */
.text { color: #595959; }
```

Tools: WebAIM Contrast Checker, axe DevTools

### Fix: Keyboard Navigation for Custom Widgets

```javascript
class AccessibleDropdown extends HTMLElement {
  connectedCallback() {
    this.setAttribute("tabindex", "0");
    this.setAttribute("role", "combobox");
    this.setAttribute("aria-expanded", "false");

    this.addEventListener("keydown", (e) => {
      switch (e.key) {
        case "Enter": case " ": this.toggle(); e.preventDefault(); break;
        case "Escape": this.close(); break;
        case "ArrowDown": this.focusNext(); e.preventDefault(); break;
        case "ArrowUp": this.focusPrevious(); e.preventDefault(); break;
      }
    });
  }
}
```

## Color Contrast Requirements

```typescript
const CONTRAST_REQUIREMENTS = {
  normalText:   { AA: 4.5, AAA: 7   },   // <18pt or <14pt bold
  largeText:    { AA: 3,   AAA: 4.5 },   // >=18pt or >=14pt bold
  uiComponents: { AA: 3             },
};
```

## Mobile Accessibility

- Touch target sizing: 44x44dp minimum (AAA), 24x24px minimum (AA, WCAG 2.2)
- VoiceOver and TalkBack compatibility
- Gesture alternatives for all swipe/pinch actions
- Dynamic Type / font scaling support

> Detailed mobile patterns: see `references/mobile-accessibility.md`

## Best Practices

- **Start early**: Accessibility from design phase, not as an afterthought
- **Use semantic HTML first**: Reduces ARIA needs — native elements are already accessible
- **Don't disable focus styles**: Style them visibly, never remove them
- **Keyboard first**: Design all interactions to work without a mouse
- **Respect user preferences**: Honor `prefers-reduced-motion` and `prefers-contrast`
- **Announce dynamic changes**: Use live regions for content updates
- **Support zoom**: Content must work at 200% zoom, reflow at 400%
- **Test with real users**: Include people with disabilities in user testing
- **Don't rely only on automation**: Automated tools catch 30–50% of issues

## Testing Tools

| Category     | Tools                                                     |
| ------------ | --------------------------------------------------------- |
| Automated    | axe DevTools, WAVE, Lighthouse, pa11y                     |
| Screen readers | VoiceOver (macOS/iOS), NVDA/JAWS (Windows), TalkBack (Android) |
| Simulators   | NoCoffee (vision), Silktide (various disabilities)        |
| Contrast     | WebAIM Contrast Checker, Colour Contrast Analyser         |
| CLI          | `@axe-core/cli`, `pa11y`, `lighthouse`                    |

## Resources

- [WCAG 2.2 Quick Reference](https://www.w3.org/WAI/WCAG22/quickref/)
- [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)
- [A11y Project Checklist](https://www.a11yproject.com/checklist/)
- [Inclusive Components](https://inclusive-components.design/)
- [Deque University](https://dequeuniversity.com/)
- [WebAIM](https://webaim.org/)
