# Accessibility

> Accessibility rules - WCAG 2.2, POUR principles, color contrast, keyboard navigation, focus management, ARIA roles and attributes, screen reader testing, axe-core, reduced motion

- Skill: `14bryanespinoza/accessibility` (Agent Skill)
- Install (CLI): `npx skillmds@latest add 14bryanespinoza/accessibility`
- Raw SKILL.md: https://api.skillmd.com/api/skills/14bryanespinoza/accessibility/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: 14BryanEspinoza (https://skillmd.com/u/14bryanespinoza)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/14bryanespinoza/accessibility

---


# Accessibility — Rules and Conventions

---

## 1. Philosophy

1. **WCAG 2.2 AA minimum** — Legal baseline for most jurisdictions. Aim for AAA where feasible.
2. **POUR principles** — Perceivable, Operable, Understandable, Robust. Every decision maps here.
3. **Semantic HTML first** — Native elements carry accessibility. ARIA only when HTML insufficient.
4. **Test with users** — Automated tools catch ~30% of issues. Manual + screen reader testing required.
5. **Progressive enhancement** — Core functionality works without JS. Enhance, don't depend.

---

## 2. Minimum Versions

| Technology | Minimum Version |
| ---------- | --------------- |
| axe-core   | 4.8+            |
| Node.js    | 22+             |
| pnpm       | 11+             |

---

## 3. WCAG 2.2 & POUR (Compact)

| Principle          | Guideline            | Key Success Criteria (AA)                                                                          |
| ------------------ | -------------------- | -------------------------------------------------------------------------------------------------- |
| **Perceivable**    | 1.1 Non-text content | 1.1.1 Alt text for images                                                                          |
|                    | 1.3 Adaptable        | 1.3.1 Info/relationships, 1.3.4 Orientation                                                        |
|                    | 1.4 Distinguishable  | 1.4.3 Contrast (4.5:1), 1.4.4 Resize text, 1.4.11 Non-text contrast (3:1)                          |
| **Operable**       | 2.1 Keyboard         | 2.1.1 Keyboard, 2.1.2 No trap, 2.1.4 Shortcuts                                                     |
|                    | 2.4 Navigable        | 2.4.3 Focus order, 2.4.7 Focus visible, 2.4.11 Focus not obscured                                  |
|                    | 2.5 Input modalities | 2.5.1 Pointer gestures, 2.5.2 Pointer cancellation, 2.5.3 Label in name, 2.5.8 Target size (24×24) |
| **Understandable** | 3.2 Predictable      | 3.2.1 On focus, 3.2.2 On input                                                                     |
|                    | 3.3 Input assistance | 3.3.1 Error identification, 3.3.2 Labels/instructions, 3.3.3 Error suggestion                      |
| **Robust**         | 4.1 Compatible       | 4.1.2 Name/role/value, 4.1.3 Status messages                                                       |

> Full WCAG 2.2: w3.org/WAI/WCAG22/quickref/

---

## 4. Color Contrast

### Minimum ratios (WCAG 2.2 AA)

| Element                              | Ratio | AAA   |
| ------------------------------------ | ----- | ----- |
| Normal text (< 24px / < 18.5px bold) | 4.5:1 | 7:1   |
| Large text (≥ 24px / ≥ 18.5px bold)  | 3:1   | 4.5:1 |
| UI components (borders, icons)       | 3:1   | —     |
| Focus indicators                     | 3:1   | —     |

### Tools

```bash
# Automated
pnpm add -D axe-core @axe-core/cli
npx axe http://localhost:3000

# Design-time
# Figma: Stark, Contrast plugins
# Browser: axe DevTools, WAVE
```

### Rules

- **Never rely on color alone** — add text, icon, or pattern
- **Test in forced-colors mode** — `@media (forced-colors: active)`
- **High contrast mode** — ensure visibility

---

## 5. Keyboard Navigation

### Focus order

```html
<!-- Logical DOM order = visual order -->
<header>...</header>
<main>...</main>
<aside>...</aside>
<footer>...</footer>
```

### Focus trap (modals, drawers)

```js
function trapFocus(element) {
  const focusable = element.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
  );
  const first = focusable[0];
  const last = focusable[focusable.length - 1];

  element.addEventListener("keydown", (e) => {
    if (e.key !== "Tab") return;
    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault();
      last.focus();
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault();
      first.focus();
    }
  });
  first?.focus();
}
```

### Skip link (first focusable element)

```html
<a href="#main" class="skip-link">Skip to main content</a>
```

```css
.skip-link {
  position: absolute;
  top: -100%;
  left: 50%;
  transform: translateX(-50%);
  padding: 1rem 2rem;
  background: var(--color-primary);
  color: white;
  z-index: 10000;
}
.skip-link:focus {
  top: 1rem;
}
```

### Rules Navigation

- **All interactive elements reachable** — no `tabindex="-1"` unless intentional
- **No keyboard traps** — `Esc` closes modals/drawers
- **Visible focus** — never `outline: none` without replacement

---

## 6. Focus Management

### Focus restoration

```js
function openModal(modal) {
  const previousFocus = document.activeElement;
  modal.show();
  modal.addEventListener("hidden", () => previousFocus?.focus(), {
    once: true,
  });
}
```

### Focus visible (CSS)

```css
:focus-visible {
  outline: 3px solid var(--color-focus, #0066cc);
  outline-offset: 2px;
}

/* Remove default :focus only if :focus-visible provided */
:focus:not(:focus-visible) {
  outline: none;
}
```

### Focus not obscured (WCAG 2.2 2.4.11)

```css
/* Ensure focus indicator not hidden by sticky headers */
:focus-visible {
  scroll-margin-top: 100px; /* height of sticky header */
}
```

---

## 7. ARIA Essentials

### Roles (essential only)

| Role                       | Use                    | Native alternative           |
| -------------------------- | ---------------------- | ---------------------------- |
| `button`                   | Clickable non-button   | `<button>`                   |
| `dialog`                   | Modal dialog           | `<dialog>`                   |
| `alert`                    | Urgent message         | —                            |
| `status`                   | Non-urgent live update | —                            |
| `navigation`               | Nav section            | `<nav>`                      |
| `main`                     | Main content           | `<main>`                     |
| `region`                   | Labeled section        | `<section aria-label="...">` |
| `tablist`/`tab`/`tabpanel` | Tabs                   | —                            |
| `menu`/`menuitem`          | Custom menu            | —                            |
| `listbox`/`option`         | Custom select          | `<select>`                   |
| `combobox`                 | Autocomplete           | `<input list="...">`         |
| `tooltip`                  | Hover/focus label      | `title` attribute            |
| `search` / `searchbox`     | Search                 | `<form role="search">`       |

### States & Properties (essential)

| Attribute          | Values                              | Use                            |
| ------------------ | ----------------------------------- | ------------------------------ |
| `aria-expanded`    | `true/false`                        | Disclosure state               |
| `aria-controls`    | IDREF                               | Element controlled             |
| `aria-labelledby`  | IDREF                               | Accessible name from element   |
| `aria-describedby` | IDREF                               | Additional description         |
| `aria-hidden`      | `true/false`                        | Hide from AT                   |
| `aria-live`        | `polite/assertive/off`              | Live region                    |
| `aria-invalid`     | `true/false/grammar/spelling`       | Form validation                |
| `aria-required`    | `true/false`                        | Required field                 |
| `aria-disabled`    | `true/false`                        | Disabled (not `disabled` attr) |
| `aria-current`     | `page/step/location/date/time/true` | Current item                   |
| `aria-label`       | string                              | Name when no visible label     |
| `aria-orientation` | `vertical/horizontal`               | Slider, tabs                   |

### Rules ARiA

- **Native HTML first** — `<button>` not `<div role="button">`
- **No redundant ARIA** — `<nav aria-label="Main">` not `<nav role="navigation" aria-label="Main">`
- **ID references must exist** — `aria-controls="id"` requires `id="id"`

---

## 8. Accessible Name Computation

```html
<!-- 1. aria-labelledby (highest priority) -->
<input aria-labelledby="label-id" />
<span id="label-id">Email</span>

<!-- 2. aria-label -->
<button aria-label="Close">×</button>

<!-- 3. <label for> -->
<label for="email">Email</label>
<input id="email" />

<!-- 4. Inner text (buttons, links) -->
<button>Submit</button>

<!-- 5. title attribute (fallback) -->
<input title="Search" />
```

---

## 9. Live Regions

```html
<!-- Polite: queued, non-interrupting -->
<div aria-live="polite" aria-atomic="true">Item added to cart</div>

<!-- Assertive: immediate interrupt -->
<div aria-live="assertive">Error: Payment failed</div>

<!-- Status: implied polite -->
<div role="status">Loading...</div>

<!-- Log: sequential updates -->
<div role="log" aria-live="polite">Message 1<br />Message 2</div>
```

### Rules Live Regions

- **`aria-atomic="true"`** — read entire region on change
- **Don't overuse** — only for dynamic updates user must know
- **Clear on dismiss** — empty region after user acknowledges

---

## 10. Accessible Forms

### Label association

```html
<!-- Explicit (preferred) -->
<label for="email">Email</label>
<input type="email" id="email" required />

<!-- Implicit -->
<label>Email <input type="email" required /></label>
```

### Validation & errors

```html
<div>
  <label for="email">Email <span aria-hidden="true">*</span></label>
  <input
    type="email"
    id="email"
    required
    aria-describedby="email-error"
    aria-invalid="true"
  />
  <span id="email-error" role="alert">Invalid email format</span>
</div>
```

### Rules Accessible Forms

- **Every input has a label** — visible or `aria-label`/`aria-labelledby`
- **Required marked** — `required` attr + visual indicator (`*`)
- **Errors announced** — `role="alert"` or `aria-live="assertive"`
- **Error linked** — `aria-describedby` on input
- **Group related** — `<fieldset>` + `<legend>` for radios/checkboxes

---

## 11. Modals & Dialogs

```html
<dialog id="modal" aria-modal="true" aria-labelledby="modal-title">
  <h2 id="modal-title">Confirm</h2>
  <p>Are you sure?</p>
  <menu>
    <button id="cancel">Cancel</button>
    <button id="confirm" autofocus>Confirm</button>
  </menu>
</dialog>
```

```js
const modal = document.getElementById("modal");
const previousFocus = document.activeElement;

modal.showModal();
trapFocus(modal);

modal.addEventListener(
  "close",
  () => {
    previousFocus?.focus();
  },
  { once: true },
);

document.getElementById("cancel").onclick = () => modal.close();
document.getElementById("confirm").onclick = () => modal.close("confirmed");
```

### Rules Modals & Dialog

- **`<dialog>` native** — `aria-modal="true"`, `showModal()` traps focus
- **Focus trap** — `Tab` cycles within, `Esc` closes
- **Return focus** — to trigger element on close
- **No scroll behind** — `body { overflow: hidden }` while open

---

## 12. Reduced Motion

```css
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
```

```js
// Respect in JS
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
if (!prefersReduced) {
  element.animate([...], { duration: 300 })
}
```

### Rules Reduce Motion

- **Respect `prefers-reduced-motion`** — disable non-essential motion
- **Essential motion OK** — loading spinners, progress indicators
- **Provide pause/stop** — for auto-playing carousels, videos

---

## 13. Testing

### Automated (CI)

```bash
# axe-core
pnpm add -D axe-core @axe-core/cli
npx axe http://localhost:3000 --save --exit

# Lighthouse CI
pnpm add -D @lhci/cli
npx lhci autorun
```

### Manual checklist (essential)

- [ ] Tab through entire page — all interactive reachable
- [ ] Focus visible on all elements
- [ ] Screen reader (NVDA/VoiceOver) — announce correctly
- [ ] Zoom 200% — no horizontal scroll, content readable
- [ ] High contrast mode — all UI visible
- [ ] `prefers-reduced-motion` — animations disabled
- [ ] Forms — labels, errors, required announced
- [ ] Modals — focus trap, restore, `Esc` closes
- [ ] Landmarks — `main`, `nav`, `aside`, `header`, `footer` present
- [ ] Language declared — `<html lang="en">`

---

## 14. Methodology

Before using ANY accessibility pattern not documented in
this skill:

1. **MCP Context7** (priority): `context7_resolve-library-id` +
   `context7_query-docs` for axe-core, WAI-ARIA.
2. **Official docs**: w3.org/WAI, developer.mozilla.org
   — verify current specs.
3. **Project config**: `package.json`, CI configs
   — verify against actual setup.
4. **HARD RULE**: If not in this skill AND cannot be verified against
   2 authoritative sources → DO NOT USE IT. Document as assumption or risk in
   report to orchestrator.

---

## 15. Prohibitions

- ❌ Do not use `role="button"` on `<a>` — use `<button>` for actions
- ❌ Do not use `aria-hidden="true"` on focusable elements
- ❌ Do not use `tabindex > 0` — breaks natural order
- ❌ Do not skip heading levels (`h1` → `h3`)
- ❌ Do not use color alone for status (error/success)
- ❌ Do not autoplay audio/video > 3s without pause
- ❌ Do not use `title` attribute as sole label
- ❌ Do not disable zoom (`user-scalable=no`)

---

## 16. References

> **Note:** For HTML conventions (semantics, landmarks),
> see [HTML](../html/SKILL.md)
> **Note:** For CSS conventions (contrast, focus, reduced motion),
> see [CSS](../css/SKILL.md)
> **Note:** For JavaScript conventions (focus management),
> see [JavaScript](../javascript/SKILL.md)
> **Note:** For Component Design (APG patterns),
> see [Component Design](../component-design/SKILL.md)

---

Last updated: 2026-08

