principle-accessibility · git:20260828.71a6399 · 2026-08-28 · sha256 295f5ec139fe3b1b
principle-accessibility git:20260828.71a6399A
Immutable. This exact content never changes and is served at /api/v1/blob/295f5ec139fe3b1b.
--- name: principle-accessibility description: Accessibility (a11y) principles — WCAG 2.2 AA conformance, semantic HTML, keyboard navigation, focus management, focus traps, color contrast, alt text, screen reader compatibility, reduced motion. Auto-load when reviewing frontend or UI code, evaluating ARIA usage, auditing color contrast, building modals or dialogs, choosing between semantic elements and ARIA workarounds, or assessing screen-reader experience. --- <!-- preload-canary: SWB-PRELOAD-PRINCIPLE-ACCESSIBILITY --> # Accessibility Principles Accessibility bugs are correctness bugs. They are cheapest to fix before the first line of markup is written. ## Semantic HTML First Native elements carry built-in roles, keyboard behavior, and accessible names — no ARIA required. - Prefer `<button>` over `<div role="button">`; prefer `<a href>` over `<span onClick>`. Native elements are accessible by default. - Use landmark elements (`<main>`, `<nav>`, `<header>`, `<footer>`, `<aside>`) to define regions screen readers can navigate directly. - Maintain a logical heading hierarchy (`h1` → `h2` → `h3`); never skip levels to achieve visual styling. - Mark up lists with `<ul>`/`<ol>` + `<li>` — screen readers announce item count and position. - Use `<table>` with `<th scope>` and `<caption>` for tabular data; never for layout. ## ARIA: Use Sparingly The first rule of ARIA: do not use ARIA if a native HTML element already provides the semantics. - Never add redundant roles (`<button role="button">`) — they add noise without benefit. - Every ARIA widget role has required owned elements and states; a `listbox` without `option` children is broken. - Live regions (`aria-live`, `aria-atomic`) announce dynamic changes — use `assertive` only for time-critical alerts; default to `polite`. - `aria-label` / `aria-labelledby` / `aria-describedby` are the canonical tools for providing accessible names when native labeling is insufficient. - `aria-hidden="true"` on a focusable element is a contradiction — the element is both hidden from the tree and reachable by keyboard. ## Keyboard Navigation & Focus Every user who cannot use a pointer must be able to reach and operate every interactive element via keyboard alone. - All interactive elements must be reachable by Tab and operable by Enter/Space; use arrow keys for composite widgets (menus, tabs, grids). - Preserve DOM source order as the logical tab order; avoid `tabindex` values > 0 (they override natural order globally). - Visible focus indicators are required (WCAG 2.2 SC 2.4.11 AA): the indicator must have ≥3:1 contrast against adjacent colors *and* sufficient area (at minimum, a solid 2px outline around the component perimeter). A thin 1px outline at 3:1 passes contrast but fails on area. - Modals must trap focus while open — Tab cycles through focusable children; Escape closes and restores focus to the trigger element. - Skip links (`<a href="#main-content">`) let keyboard users bypass repeated navigation; place as the first focusable element on the page. ## Color, Contrast & Motion Visual information conveyed by color alone is inaccessible to color-blind users. - WCAG 2.2 AA contrast ratios: 4.5:1 for normal text, 3:1 for large text (≥18pt or ≥14pt bold), 3:1 for UI components and graphical objects. - Never use color as the only means to convey meaning — pair with text, icon, or pattern (e.g., error = red + icon + label). - Respect `prefers-reduced-motion`: declare animations normally, then disable or reduce them inside `@media (prefers-reduced-motion: reduce)`. The opt-in pattern (`no-preference`) is fragile in environments that do not evaluate media queries. - WCAG 2.2 SC 2.5.8: touch/pointer targets must be ≥24×24 CSS px, or the offset from every adjacent target must be sufficient that a 24 px circle centred on each target does not intersect another. ## Screen Reader & Assistive Tech Automated tools (axe, Lighthouse) catch ~40% of issues; manual testing with a real screen reader catches the rest. - Every `<img>` needs an `alt` attribute — empty (`alt=""`) for decorative images, descriptive text for informative ones. - Every form input needs an associated `<label>` via `for`/`id` or `aria-labelledby` — `placeholder` is not a label substitute. - Status messages (save confirmation, cart update) must be announced without stealing focus: use a live region or `role="status"`. - Test with VoiceOver (macOS/iOS), NVDA or JAWS (Windows) — screen reader support for ARIA diverges. axe passing is necessary but not sufficient. ## When Accessibility Engineering is Overkill - Purely internal CLI tools with no graphical interface. - Throwaway prototypes that will never be seen by end users. - Admin tools with a documented legal exemption covering the specific user population. - Public web and native UI: never overkill regardless of timeline or scope pressure. ## Red Flags | Flag | Problem | |------|---------| | `<div onClick>` or `<span onClick>` without `role` + `tabindex` | Not keyboard-reachable; not announced to screen readers | | Missing `alt` attribute on `<img>` | Image content invisible to screen readers | | `tabindex="-1"` on an element with no roving-tabindex parent and no programmatic `focus()` call | Silently unreachable: excluded from sequential tab order and nothing focuses it programmatically | | Modal without a focus trap | Tab escapes the dialog; background content becomes operable | | Color-only error signaling | Color-blind users miss the error state entirely | | `placeholder` used as the visible label | Disappears on input; fails contrast at default browser styles | | `autofocus` on page load without user consent | Disorienting for screen reader users who have not yet reached that element | | `aria-hidden="true"` on a focused element | Element is simultaneously hidden from the accessibility tree and keyboard-reachable | | Missing or low-contrast focus indicator | Keyboard users lose their position in the page |