| name | accessibility-expert |
| description | Expert web accessibility (a11y): WCAG 2.2 AA, semantic HTML, ARIA, keyboard, and screen readers. Trigger keywords: accessibility, a11y, WCAG, ARIA, role, screen reader, keyboard navigation, focus, focus trap, contrast, alt text, semantic HTML, label, axe, tab order. Use to build accessible UI, audit/fix a11y issues, or add accessible behavior to widgets. |
Web Accessibility Expert
The first rule of ARIA is don't use ARIA — a native <button> beats <div role="button"> every time. Target WCAG 2.2 AA. If it doesn't work with the keyboard, it isn't done.
When to Use
- Building UI you want accessible from the start.
- Auditing/fixing issues: keyboard traps, unlabeled controls, low contrast, missing focus.
- Adding accessible behavior to custom widgets (modal, menu, tabs, combobox, accordion, tooltip).
- Forms, images, dynamic updates, and motion.
When NOT to Use
- Pure visual styling without semantics →
tailwind-expert.
- Component state logic →
react-expert.
Core Principles
1. Semantics first, ARIA last
- Use the native element for the job (
<button>, <a href>, <label>, <nav>, <main>, <dialog>, <input type>). They bring role, state, and keyboard behavior for free.
- No ARIA is better than bad ARIA. Don't add roles to elements that already have them; don't override native semantics. ARIA changes how AT announces — it does not add behavior (you still wire keyboard/focus yourself).
2. Keyboard operability (WCAG 2.1.1)
- Everything actionable is reachable and operable by keyboard: Tab to move, Enter/Space to activate, Esc to dismiss, arrows within composite widgets (menus, tabs, radios).
- Keep a visible focus indicator (
:focus-visible). Never outline:none without a clear replacement.
- Logical tab order = DOM order. Avoid positive
tabindex; use tabindex="-1" for programmatic focus, 0 to add non-interactive elements to tab order only when necessary.
3. Focus management for overlays
- Dialogs/menus: move focus in on open, trap within, restore to the trigger on close, and make background
inert/aria-hidden. Prefer the native <dialog> element.
4. Name, role, value (WCAG 4.1.2)
- Every control has an accessible name:
<label for>, wrapping <label>, aria-label, or aria-labelledby. Icon-only buttons need an accessible name (visually-hidden text or aria-label).
- Images: meaningful
alt; decorative → alt="". Communicate state with aria-expanded/-selected/-checked/-current/-pressed, and announce async changes via a live region (aria-live="polite", or role="alert" for errors).
5. Perceivable
- Contrast ≥ 4.5:1 (text), 3:1 (large text & UI components/focus indicators — new in 2.2). Never rely on color alone to convey meaning.
- Honor
prefers-reduced-motion. Don't disable zoom; support 200% text scaling.
Common Mistakes
<div onClick> as a button → not focusable or keyboard-operable; use <button>.
- Placeholder as a label → disappears on input; use a real
<label>.
role without behavior → role="button" still needs tabindex="0" + Enter/Space handlers (so just use <button>).
- Removing focus outlines for looks → keyboard users get lost.
aria-label on a non-interactive <div> → often ignored; put names on real controls.
- Toast/error not announced → wrap in a live region.
tabindex > 0 → breaks natural order.
Examples
Accessible icon button + disclosure
<button type="button" aria-expanded="false" aria-controls="menu">
<svg aria-hidden="true" focusable="false">…</svg>
<span class="sr-only">Open menu</span>
</button>
<ul id="menu" hidden>…</ul>
Form field wired to its label and error
<label for="email">Email</label>
<input id="email" name="email" type="email"
aria-describedby="email-err" aria-invalid="true" required />
<p id="email-err" role="alert">Enter a valid email address.</p>
See Also
tailwind-expert — focus-visible styles and meeting contrast ratios.
react-expert — focus management, inert, and accessible component patterns.
testing-expert — automated checks with axe-core/jest-axe in CI.