a11y-default-review
Installation
SKILL.md
A11y default review
The site works for everyone or it doesn't work.
Accessibility regressions ship by default unless someone is looking. After a UI change, run this checklist before declaring it done. The bar is WCAG 2.1 AA — the same bar mateonunez.co is built to.
This is a review skill, not a teaching skill. It assumes you know what aria-live does; it tells you when to look for it.
When this skill is active
You just edited:
- A React/Vue/Svelte component that renders interactive elements
- A page route or layout
- Anything with
onClick,onKeyDown, or focus management - A form, modal, drawer, menu, tooltip, or toast
- Any element using
tabindexoraria-*
The checklist
Keyboard
- Every interactive element is reachable by
Tabin source order. -
EnterandSpaceactivate buttons;Enterfollows links. -
Escapecloses modals, drawers, menus, popovers. - Arrow keys work where expected (menu, listbox, tablist, slider).
- No keyboard trap —
Tabcan always escape.
Focus visibility
-
:focus-visibleoutline is visible against the background. Nooutline: nonewithout a replacement. - Focus moves into a modal when it opens; returns to the trigger when it closes.
- Skip-nav link exists on pages with substantial top-of-page chrome.
Semantics
- Buttons are
<button>, not<div onClick>. Links are<a href>, not<div onClick>. - One
<h1>per page. Heading levels don't skip (h2 → h4 is wrong). - Form fields have associated labels (
<label for>or wrapping<label>). - Images have
alt(emptyalt=""for decorative; descriptive for content). - Lists use
<ul>/<ol>/<li>, not<div>siblings.
ARIA (only if semantics aren't enough)
- No
aria-labelon a button that already has visible text — duplicates the announcement. -
aria-live="polite"on async status regions (search results count, save indicators). -
aria-expanded,aria-controlson disclosure widgets. -
role="dialog"+aria-modal="true"+aria-labelledbyon modals. - No
rolethat contradicts the element (e.g.role="button"on an<a href>).
Motion + colour
- Animations respect
prefers-reduced-motion. Critical motion is gated. - Colour is not the only signal (e.g. error state has an icon or text, not just red).
- Text contrast ≥ 4.5:1 (3:1 for large text). Check against the actual background.
Async + dynamic
- Loading states announce ("Loading…" via
aria-liveorrole="status"). - Errors are announced, not just rendered silently.
- Toasts/alerts are reachable or auto-dismiss with enough time (≥ 5s default).
Anti-patterns
<div onClick>— not focusable, not keyboard-activatable, not announced as a button. Use<button>.<a>withhref="#"for buttons — use<button>. Reserve<a>for navigation.tabindex> 0 — breaks source-order focus. Almost always wrong.aria-hidden="true"on a focusable element — invisible to AT but reachable by keyboard. Useinertinstead, or remove from tab order.- Auto-playing video/audio with sound — WCAG 1.4.2 violation.
- Placeholder-as-label. Placeholders disappear on focus and have low contrast. Use a real
<label>. alt="image of a cat"— screen readers already announce "image". Justalt="a cat sleeping".
When you find issues
Don't bundle the fixes into an unrelated PR. Note them, prioritise (keyboard > semantics > ARIA polish), and either fix in a separate commit or surface to me as a follow-up. A11y debt is real debt — track it.
If it doesn't hold up for a keyboard-only or screen-reader user, it doesn't make the cut.
Related skills