Accessibility audit
Accessibility is baseline, not a feature. Every pattern must be usable without a mouse, without sight, and without fine motor control.
Focus areas
- Semantic HTML first — use native elements (
<button>, <a>, <details>, <dialog>) where possible.
- Keyboard access and visible focus indicators on all interactive elements.
- Labels, landmarks, and sensible heading hierarchy.
- Contrast and motion preferences respected in light and dark mode.
Page structure
- One
<h1> per page, no skipped heading levels.
- Landmarks:
<nav aria-label="…">, <main id="main">, <article>, <section aria-labelledby="…">, <aside>, <footer>.
- Skip link to
#main in the layout.
- Set
lang on <html>; derive per locale where i18n is in use.
Interactive elements
Links
- Links distinguishable by underline, not color alone.
- External links with
target="_blank" need rel="noopener noreferrer" and a screen reader hint (<span class="sr-only">(opens in new tab)</span>).
- Icon-only links need
aria-label; the icon inside gets aria-hidden="true".
- Never use
<a> without href for actions — use <button type="button">.
Buttons
- Icon-only buttons must have
aria-label; inner icon aria-hidden="true".
- Always set
type="button" or type="submit" explicitly.
- Never
<div onclick> — use <button>.
Keyboard navigation
/* Global focus style — never remove without a visible alternative */
:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
- Focus order follows visual layout — don't use positive
tabindex.
- Custom interactive elements need
tabindex="0", appropriate role, and keyboard event handlers.
Touch targets (WCAG 2.5.8)
Minimum 44×44 px for all interactive elements. Apply padding or min-h-[44px] min-w-[44px].
Forms
<div>
<label for="email" class="text-sm font-semibold">
Email <span aria-hidden="true">*</span>
</label>
<input
type="email"
id="email"
name="email"
required
autocomplete="email"
aria-invalid={!!inputError?.email}
aria-describedby={inputError?.email ? 'email-error' : undefined}
/>
{inputError?.email && (
<span id="email-error" class="text-xs text-error" role="alert">
{inputError.email}
</span>
)}
</div>
Checklist:
| Requirement |
Implementation |
| Associated label |
<label for="id"> matching <input id="id"> |
| Required indicator |
Visual * with aria-hidden="true" plus required |
| Error state |
aria-invalid={true} on invalid input |
| Error message |
<span role="alert"> linked via aria-describedby |
| Autocomplete |
autocomplete="name|email|tel|..." |
| Grouped inputs |
<fieldset> + <legend> for radios/checkboxes/address blocks |
| Success feedback |
<div role="status"> |
Tables
<caption> for purpose, <th scope="col"> for columns, <th scope="row"> for row headers.
- Never use tables for layout.
Images
- All
<img> need alt. Decorative: alt="" plus aria-hidden="true".
- Always set
width and height to prevent layout shift.
- SVG icons:
aria-hidden="true" focusable="false" when decorative.
Color & contrast
| Text type |
Minimum ratio |
| Normal text (< 18 px) |
4.5:1 |
| Large text (>= 18 px bold / 24 px) |
3:1 |
| UI components, borders, icons, focus rings |
3:1 |
- Never use color as the sole indicator — pair with icon, text, or pattern.
- Every color class needs an explicit
dark: counterpart; both modes must independently meet contrast.
- Avoid low-contrast muted grays on white (e.g.
text-neutral-400).
Motion
All animations must respect prefers-reduced-motion:
.animate-fade-in { animation: fadeIn 0.3s ease-out; }
@media (prefers-reduced-motion: reduce) {
.animate-fade-in { animation: none; }
}
- No blinking or flashing effects (WCAG 2.3.1 — max 3 flashes/sec).
- Tailwind: use
motion-safe: and motion-reduce: variants.
- Scroll-driven animations: provide
@supports fallback plus reduced-motion override.
ARIA patterns
Navigation with aria-current
<a href={href} aria-current={isActive ? 'page' : undefined}>{label}</a>
Disclosure / accordion
Prefer native <details> + <summary>. For custom accordions use aria-expanded and aria-controls.
Tabs
role="tablist" with aria-label on container.
role="tab" + aria-selected on each tab; inactive tabs tabindex="-1".
role="tabpanel" + aria-labelledby on each panel.
- Arrow-key navigation left/right.
Dialog
Prefer native <dialog>. For custom modals:
role="dialog" + aria-modal="true" + aria-labelledby.
- Focus trap, Escape closes, return focus to trigger on close.
Live regions
aria-live="polite" for non-urgent status.
role="alert" for errors (assertive).
role="status" for success messages.
Lists
Tailwind's list-none strips list semantics in Safari — add role="list" when you need the semantic back.
Anti-patterns
<!-- WRONG --> <div class="cursor-pointer" me</div>
<!-- RIGHT --> <button type="button">Click me</button>
<!-- WRONG --> <button><svg>…</svg></button>
<!-- RIGHT --> <button aria-label="Close menu"><svg aria-hidden="true">…</svg></button>
<!-- WRONG --> <span class="text-red-500">Error</span>
<!-- RIGHT --> <span class="text-red-500">⚠ Error: email is required</span>
<!-- WRONG --> <p class="text-neutral-700">Text</p>
<!-- RIGHT --> <p class="text-neutral-700 dark:text-neutral-300">Text</p>
New page checklist
lang set on <html>.
- Single
<h1>, sensible heading hierarchy.
- Semantic landmarks in place, skip link works.
- All images have
alt (or alt="" + aria-hidden).
- All links have visible underline (or button styling is intentional).
- Icon-only buttons/links have
aria-label.
- Form inputs:
<label>, aria-invalid, aria-describedby, autocomplete.
- Fieldset + legend for grouped inputs.
- Error messages use
role="alert"; success uses role="status".
- Focus indicators visible, no keyboard traps, all interactive elements reachable.
- Contrast meets 4.5:1 / 3:1 and holds in dark mode.
- No color-only signals.
- Touch targets ≥ 44×44 px.
- Animations respect reduced motion.
- External links:
rel="noopener noreferrer".
- Tables:
<caption> and <th scope>.
Testing
Automated (axe-core + Playwright)
import AxeBuilder from '@axe-core/playwright';
test('page has no a11y violations', async ({ page }) => {
await page.goto('/new-page');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
.analyze();
expect(results.violations).toEqual([]);
});
Build-time
@casoon/astro-post-audit checks after every build for missing alt text, missing labels, empty button text, missing skip link, and heading hierarchy violations.
Manual
- Tab through the page — verify focus order and visibility.
- Screen reader (VoiceOver / NVDA / Orca).
- Zoom to 200 % — content must reflow without horizontal scroll.
- Enable
prefers-reduced-motion in DevTools — animations must stop.
- DevTools Accessibility tab — check computed roles and names.
1---2name: accessibility-audit3description: WCAG 2.2 AA patterns for Astro — semantic HTML, keyboard navigation, focus indicators, form labels, contrast ratios, ARIA roles, and motion preferences. Use when building or auditing any UI for accessibility, adding forms or interactive components, reviewing contrast or touch targets, or when fixing axe-core violations from Playwright tests. Load this skill before adding any form, dialog, tab, accordion, or icon-only button.4---56# Accessibility audit78Accessibility is baseline, not a feature. Every pattern must be usable without a mouse, without sight, and without fine motor control.910## Focus areas1112- Semantic HTML first — use native elements (`<button>`, `<a>`, `<details>`, `<dialog>`) where possible.13- Keyboard access and visible focus indicators on all interactive elements.14- Labels, landmarks, and sensible heading hierarchy.15- Contrast and motion preferences respected in light and dark mode.1617## Page structure1819- One `<h1>` per page, no skipped heading levels.20- Landmarks: `<nav aria-label="…">`, `<main id="main">`, `<article>`, `<section aria-labelledby="…">`, `<aside>`, `<footer>`.21- Skip link to `#main` in the layout.22- Set `lang` on `<html>`; derive per locale where i18n is in use.2324## Interactive elements2526### Links2728- Links distinguishable by underline, not color alone.29- External links with `target="_blank"` need `rel="noopener noreferrer"` and a screen reader hint (`<span class="sr-only">(opens in new tab)</span>`).30- Icon-only links need `aria-label`; the icon inside gets `aria-hidden="true"`.31- Never use `<a>` without `href` for actions — use `<button type="button">`.3233### Buttons3435- Icon-only buttons must have `aria-label`; inner icon `aria-hidden="true"`.36- Always set `type="button"` or `type="submit"` explicitly.37- Never `<div onclick>` — use `<button>`.3839### Keyboard navigation4041```css42/* Global focus style — never remove without a visible alternative */43:focus-visible {44 outline: 2px solid var(--color-accent);45 outline-offset: 2px;46}47```4849- Focus order follows visual layout — don't use positive `tabindex`.50- Custom interactive elements need `tabindex="0"`, appropriate `role`, and keyboard event handlers.5152### Touch targets (WCAG 2.5.8)5354Minimum 44×44 px for all interactive elements. Apply padding or `min-h-[44px] min-w-[44px]`.5556## Forms5758```astro59<div>60 <label for="email" class="text-sm font-semibold">61 Email <span aria-hidden="true">*</span>62 </label>63 <input64 type="email"65 id="email"66 name="email"67 required68 autocomplete="email"69 aria-invalid={!!inputError?.email}70 aria-describedby={inputError?.email ? 'email-error' : undefined}71 />72 {inputError?.email && (73 <span id="email-error" class="text-xs text-error" role="alert">74 {inputError.email}75 </span>76 )}77</div>78```7980Checklist:8182| Requirement | Implementation |83|---|---|84| Associated label | `<label for="id">` matching `<input id="id">` |85| Required indicator | Visual `*` with `aria-hidden="true"` plus `required` |86| Error state | `aria-invalid={true}` on invalid input |87| Error message | `<span role="alert">` linked via `aria-describedby` |88| Autocomplete | `autocomplete="name\|email\|tel\|..."` |89| Grouped inputs | `<fieldset>` + `<legend>` for radios/checkboxes/address blocks |90| Success feedback | `<div role="status">` |9192## Tables9394- `<caption>` for purpose, `<th scope="col">` for columns, `<th scope="row">` for row headers.95- Never use tables for layout.9697## Images9899- All `<img>` need `alt`. Decorative: `alt=""` plus `aria-hidden="true"`.100- Always set `width` and `height` to prevent layout shift.101- SVG icons: `aria-hidden="true" focusable="false"` when decorative.102103## Color & contrast104105| Text type | Minimum ratio |106|---|---|107| Normal text (< 18 px) | 4.5:1 |108| Large text (>= 18 px bold / 24 px) | 3:1 |109| UI components, borders, icons, focus rings | 3:1 |110111- Never use color as the sole indicator — pair with icon, text, or pattern.112- Every color class needs an explicit `dark:` counterpart; both modes must independently meet contrast.113- Avoid low-contrast muted grays on white (e.g. `text-neutral-400`).114115## Motion116117All animations must respect `prefers-reduced-motion`:118119```css120.animate-fade-in { animation: fadeIn 0.3s ease-out; }121122@media (prefers-reduced-motion: reduce) {123 .animate-fade-in { animation: none; }124}125```126127- No blinking or flashing effects (WCAG 2.3.1 — max 3 flashes/sec).128- Tailwind: use `motion-safe:` and `motion-reduce:` variants.129- Scroll-driven animations: provide `@supports` fallback plus reduced-motion override.130131## ARIA patterns132133### Navigation with `aria-current`134135```astro136<a href={href} aria-current={isActive ? 'page' : undefined}>{label}</a>137```138139### Disclosure / accordion140141Prefer native `<details>` + `<summary>`. For custom accordions use `aria-expanded` and `aria-controls`.142143### Tabs144145- `role="tablist"` with `aria-label` on container.146- `role="tab"` + `aria-selected` on each tab; inactive tabs `tabindex="-1"`.147- `role="tabpanel"` + `aria-labelledby` on each panel.148- Arrow-key navigation left/right.149150### Dialog151152Prefer native `<dialog>`. For custom modals:153154- `role="dialog"` + `aria-modal="true"` + `aria-labelledby`.155- Focus trap, Escape closes, return focus to trigger on close.156157### Live regions158159- `aria-live="polite"` for non-urgent status.160- `role="alert"` for errors (assertive).161- `role="status"` for success messages.162163## Lists164165Tailwind's `list-none` strips list semantics in Safari — add `role="list"` when you need the semantic back.166167## Anti-patterns168169```html170<!-- WRONG --> <div class="cursor-pointer" onclick="…">Click me</div>171<!-- RIGHT --> <button type="button">Click me</button>172173<!-- WRONG --> <button><svg>…</svg></button>174<!-- RIGHT --> <button aria-label="Close menu"><svg aria-hidden="true">…</svg></button>175176<!-- WRONG --> <span class="text-red-500">Error</span>177<!-- RIGHT --> <span class="text-red-500">⚠ Error: email is required</span>178179<!-- WRONG --> <p class="text-neutral-700">Text</p>180<!-- RIGHT --> <p class="text-neutral-700 dark:text-neutral-300">Text</p>181```182183## New page checklist184185- `lang` set on `<html>`.186- Single `<h1>`, sensible heading hierarchy.187- Semantic landmarks in place, skip link works.188- All images have `alt` (or `alt=""` + `aria-hidden`).189- All links have visible underline (or button styling is intentional).190- Icon-only buttons/links have `aria-label`.191- Form inputs: `<label>`, `aria-invalid`, `aria-describedby`, `autocomplete`.192- Fieldset + legend for grouped inputs.193- Error messages use `role="alert"`; success uses `role="status"`.194- Focus indicators visible, no keyboard traps, all interactive elements reachable.195- Contrast meets 4.5:1 / 3:1 and holds in dark mode.196- No color-only signals.197- Touch targets ≥ 44×44 px.198- Animations respect reduced motion.199- External links: `rel="noopener noreferrer"`.200- Tables: `<caption>` and `<th scope>`.201202## Testing203204### Automated (axe-core + Playwright)205206```typescript207import AxeBuilder from '@axe-core/playwright';208209test('page has no a11y violations', async ({ page }) => {210 await page.goto('/new-page');211 const results = await new AxeBuilder({ page })212 .withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])213 .analyze();214 expect(results.violations).toEqual([]);215});216```217218### Build-time219220[`@casoon/astro-post-audit`](https://github.com/casoon/astro-post-audit) checks after every build for missing alt text, missing labels, empty button text, missing skip link, and heading hierarchy violations.221222### Manual223224- Tab through the page — verify focus order and visibility.225- Screen reader (VoiceOver / NVDA / Orca).226- Zoom to 200 % — content must reflow without horizontal scroll.227- Enable `prefers-reduced-motion` in DevTools — animations must stop.228- DevTools Accessibility tab — check computed roles and names.