Ensuring Accessibility
Goal
Make every UI component and page meet WCAG 2.2 AA standards. Accessibility is a design constraint applied from the start, not an audit bolted on after implementation.
When to Use
- Always. Every component, page, and interaction should be evaluated against these standards.
- Specifically invoke this skill when building forms, interactive widgets, navigation, modals, or any component with state changes.
Instructions
1. Color Contrast
Check every text/background combination against WCAG AA thresholds:
- Normal text (below 24px regular or 18.66px bold): minimum 4.5:1 contrast ratio.
- Large text (24px+ regular or 18.66px+ bold): minimum 3:1.
- UI components (borders, icons, form controls): minimum 3:1 against adjacent colors.
Check contrast for ALL states: default, hover, focus, disabled, active. Disabled elements are exempt from contrast requirements but should still be distinguishable.
2. Keyboard Navigation
Every interactive element must be:
- Focusable: reachable via Tab (or Shift+Tab for reverse). Custom components need
tabindex="0".
- Operable: activatable via Enter or Space. Custom click handlers need
onKeyDown handling.
- Visible when focused: a visible focus ring (minimum 2px, 3:1 contrast against adjacent background). Never use
outline: none without a replacement.
Tab order must follow visual reading order. Use the DOM order to establish logical flow — don't rely on CSS order or tabindex values greater than 0.
3. Screen Reader Support
Use semantic HTML as the foundation. The correct element communicates its role without ARIA:
<!-- Good: semantic HTML communicates role -->
<button>Save changes</button>
<nav aria-label="Main navigation">
<ul>
<li><a href="/dashboard">Dashboard</a></li>
</ul>
</nav>
<!-- Bad: div soup requiring ARIA to compensate -->
<div role="button" tabindex="0" changes</div>
<div role="navigation" aria-label="Main navigation">
<div role="list">
<div role="listitem"><span
</div>
</div>
Use ARIA only when HTML semantics are insufficient:
aria-label: when visible text doesn't adequately describe the element (e.g., an icon-only button: <button aria-label="Close dialog">).
aria-describedby: to link supplementary descriptions (error messages, help text).
aria-live="polite": for dynamic content updates (toast notifications, loading states). Use assertive only for critical, time-sensitive alerts.
aria-expanded, aria-controls: for disclosure widgets (accordions, dropdowns).
4. Accessible Forms
<form novalidate>
<fieldset>
<legend>Shipping address</legend>
<div>
<label for="street">Street address</label>
<input
id="street"
type="text"
autocomplete="street-address"
aria-required="true"
aria-invalid="true"
aria-describedby="street-error"
/>
<p id="street-error" role="alert">
Street address is required.
</p>
</div>
<div>
<label for="city">City</label>
<input
id="city"
type="text"
autocomplete="address-level2"
aria-required="true"
/>
</div>
</fieldset>
<button type="submit">Continue to payment</button>
</form>
Key form rules:
- Every input has an associated
<label> via for/id pairing. Placeholder text is NOT a label.
- Group related fields with
<fieldset> and <legend>.
- Error messages are linked to their field via
aria-describedby.
- Use
aria-invalid="true" on fields that have validation errors.
- Use
aria-required="true" (or the required attribute) for mandatory fields.
- Use
autocomplete attributes for common fields (name, email, address, phone).
5. Motion and Animation
Respect user preferences for reduced motion:
@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;
}
}
- Never auto-play animations or videos without user consent.
- Avoid animations that cover more than 1/3 of the screen area.
- Provide pause/stop controls for any animation that lasts more than 5 seconds.
- Parallax scrolling and motion-heavy transitions should be disabled entirely under
prefers-reduced-motion.
6. Images and Media
- Every informative
<img> needs a descriptive alt attribute. Describe the content, not the decoration ("Chart showing revenue growth from $2M to $5M" not "chart image").
- Decorative images: use
alt="" (empty string, NOT omitting the attribute) or CSS background-image.
- Videos need captions. Audio needs transcripts.
- SVG icons: use
aria-hidden="true" when the icon is decorative or paired with visible text.
7. Testing Checklist
- Automated: run axe-core or Lighthouse accessibility audit. Fix all critical and serious issues.
- Keyboard: navigate the entire page using only Tab, Shift+Tab, Enter, Space, Escape, Arrow keys. Every interactive element must be reachable and operable.
- Screen reader: test with VoiceOver (macOS/iOS) or NVDA (Windows). Listen to the page being read — does the reading order make sense? Are interactive elements announced with their role and state?
- Zoom: zoom to 200% and 400%. Content must remain readable and functional without horizontal scrolling at 400%.
- Color: use a color blindness simulator (e.g., Chrome DevTools). Ensure information isn't conveyed by color alone.
Constraints
✅ Do
- Use semantic HTML elements first: button, a, nav, main, header, footer, section, article, aside, fieldset, legend, label, h1-h6.
- Test every interactive component with keyboard-only navigation before considering it done.
- Check color contrast for all element states (default, hover, focus, active, disabled, selected).
- Provide text alternatives for all non-decorative images, icons, and media.
- Link error messages to form fields with aria-describedby.
- Use landmark elements (main, nav, header, footer) so screen reader users can jump between page sections.
- Include a skip-to-content link as the first focusable element on pages with complex headers.
- Set the lang attribute on the html element.
❌ Don't
- DO NOT use ARIA attributes where native HTML semantics already communicate the role. A
<button> does not need role="button". A <nav> does not need role="navigation". Adding redundant ARIA is a code smell that suggests the developer doesn't understand when ARIA is needed.
- DO NOT rely solely on color to communicate state or information. Red text for errors needs an icon or a label too. Green for success needs a checkmark or the word "Success."
- DO NOT trap keyboard focus inside a component unless it's a modal dialog (and even then, return focus to the trigger when the modal closes).
- DO NOT use
tabindex values greater than 0. This overrides the natural DOM order and creates unpredictable tab sequences. Use tabindex="0" to make elements focusable in DOM order, or tabindex="-1" for programmatic focus only.
- DO NOT disable browser zoom with
user-scalable=no or maximum-scale=1. Users with low vision depend on zoom.
- DO NOT hide focus rings with
outline: none or outline: 0 without providing a visible alternative. The default browser focus ring is better than no focus ring. If you style custom focus rings, ensure they have at least 3:1 contrast.
- DO NOT use
aria-label on elements that already have visible text content. The aria-label overrides the visible text for screen readers, creating a disconnect between what sighted and non-sighted users perceive.
- DO NOT auto-play audio, video, or animation. Users with cognitive disabilities, vestibular disorders, or who are in screen-reader mode are disrupted by unexpected media.
- DO NOT use
title attributes as the primary accessible name for interactive elements. Title tooltips are inconsistent across browsers and not accessible via keyboard.
Output Format
- Semantic HTML with ARIA attributes only where necessary.
- Inline comments explaining accessibility decisions (e.g.,
<!-- aria-live region for async validation messages -->).
- A brief testing note listing what to verify manually (keyboard flow, screen reader announcement, contrast).
Dependencies
designer/designing-ui-system/SKILL.md — color tokens must meet contrast requirements.
../../frontend/building-components/SKILL.md — component implementation must follow these accessibility patterns (cross-agent contract).
1---2name: ensuring-accessibility3description: Ensure WCAG 2.2 AA compliance covering color contrast, keyboard navigation, screen reader support, accessible forms, and motion preferences.4---56# Ensuring Accessibility78## Goal910Make every UI component and page meet WCAG 2.2 AA standards. Accessibility is a design constraint applied from the start, not an audit bolted on after implementation.1112## When to Use1314- Always. Every component, page, and interaction should be evaluated against these standards.15- Specifically invoke this skill when building forms, interactive widgets, navigation, modals, or any component with state changes.1617## Instructions1819### 1. Color Contrast2021Check every text/background combination against WCAG AA thresholds:2223- **Normal text** (below 24px regular or 18.66px bold): minimum 4.5:1 contrast ratio.24- **Large text** (24px+ regular or 18.66px+ bold): minimum 3:1.25- **UI components** (borders, icons, form controls): minimum 3:1 against adjacent colors.2627Check contrast for ALL states: default, hover, focus, disabled, active. Disabled elements are exempt from contrast requirements but should still be distinguishable.2829### 2. Keyboard Navigation3031Every interactive element must be:3233- **Focusable**: reachable via Tab (or Shift+Tab for reverse). Custom components need `tabindex="0"`.34- **Operable**: activatable via Enter or Space. Custom click handlers need `onKeyDown` handling.35- **Visible when focused**: a visible focus ring (minimum 2px, 3:1 contrast against adjacent background). Never use `outline: none` without a replacement.3637Tab order must follow visual reading order. Use the DOM order to establish logical flow — don't rely on CSS `order` or `tabindex` values greater than 0.3839### 3. Screen Reader Support4041Use semantic HTML as the foundation. The correct element communicates its role without ARIA:4243```html44<!-- Good: semantic HTML communicates role -->45<button>Save changes</button>46<nav aria-label="Main navigation">47 <ul>48 <li><a href="/dashboard">Dashboard</a></li>49 </ul>50</nav>5152<!-- Bad: div soup requiring ARIA to compensate -->53<div role="button" tabindex="0" onclick="save()">Save changes</div>54<div role="navigation" aria-label="Main navigation">55 <div role="list">56 <div role="listitem"><span onclick="navigate()">Dashboard</span></div>57 </div>58</div>59```6061Use ARIA only when HTML semantics are insufficient:6263- `aria-label`: when visible text doesn't adequately describe the element (e.g., an icon-only button: `<button aria-label="Close dialog">`).64- `aria-describedby`: to link supplementary descriptions (error messages, help text).65- `aria-live="polite"`: for dynamic content updates (toast notifications, loading states). Use `assertive` only for critical, time-sensitive alerts.66- `aria-expanded`, `aria-controls`: for disclosure widgets (accordions, dropdowns).6768### 4. Accessible Forms6970```html71<form novalidate>72 <fieldset>73 <legend>Shipping address</legend>7475 <div>76 <label for="street">Street address</label>77 <input78 id="street"79 type="text"80 autocomplete="street-address"81 aria-required="true"82 aria-invalid="true"83 aria-describedby="street-error"84 />85 <p id="street-error" role="alert">86 Street address is required.87 </p>88 </div>8990 <div>91 <label for="city">City</label>92 <input93 id="city"94 type="text"95 autocomplete="address-level2"96 aria-required="true"97 />98 </div>99 </fieldset>100101 <button type="submit">Continue to payment</button>102</form>103```104105Key form rules:106- Every input has an associated `<label>` via `for`/`id` pairing. Placeholder text is NOT a label.107- Group related fields with `<fieldset>` and `<legend>`.108- Error messages are linked to their field via `aria-describedby`.109- Use `aria-invalid="true"` on fields that have validation errors.110- Use `aria-required="true"` (or the `required` attribute) for mandatory fields.111- Use `autocomplete` attributes for common fields (name, email, address, phone).112113### 5. Motion and Animation114115Respect user preferences for reduced motion:116117```css118@media (prefers-reduced-motion: reduce) {119 *,120 *::before,121 *::after {122 animation-duration: 0.01ms !important;123 animation-iteration-count: 1 !important;124 transition-duration: 0.01ms !important;125 scroll-behavior: auto !important;126 }127}128```129130- Never auto-play animations or videos without user consent.131- Avoid animations that cover more than 1/3 of the screen area.132- Provide pause/stop controls for any animation that lasts more than 5 seconds.133- Parallax scrolling and motion-heavy transitions should be disabled entirely under `prefers-reduced-motion`.134135### 6. Images and Media136137- Every informative `<img>` needs a descriptive `alt` attribute. Describe the content, not the decoration ("Chart showing revenue growth from $2M to $5M" not "chart image").138- Decorative images: use `alt=""` (empty string, NOT omitting the attribute) or CSS `background-image`.139- Videos need captions. Audio needs transcripts.140- SVG icons: use `aria-hidden="true"` when the icon is decorative or paired with visible text.141142### 7. Testing Checklist1431441. **Automated**: run axe-core or Lighthouse accessibility audit. Fix all critical and serious issues.1452. **Keyboard**: navigate the entire page using only Tab, Shift+Tab, Enter, Space, Escape, Arrow keys. Every interactive element must be reachable and operable.1463. **Screen reader**: test with VoiceOver (macOS/iOS) or NVDA (Windows). Listen to the page being read — does the reading order make sense? Are interactive elements announced with their role and state?1474. **Zoom**: zoom to 200% and 400%. Content must remain readable and functional without horizontal scrolling at 400%.1485. **Color**: use a color blindness simulator (e.g., Chrome DevTools). Ensure information isn't conveyed by color alone.149150## Constraints151152### ✅ Do153- Use semantic HTML elements first: button, a, nav, main, header, footer, section, article, aside, fieldset, legend, label, h1-h6.154- Test every interactive component with keyboard-only navigation before considering it done.155- Check color contrast for all element states (default, hover, focus, active, disabled, selected).156- Provide text alternatives for all non-decorative images, icons, and media.157- Link error messages to form fields with aria-describedby.158- Use landmark elements (main, nav, header, footer) so screen reader users can jump between page sections.159- Include a skip-to-content link as the first focusable element on pages with complex headers.160- Set the lang attribute on the html element.161162163### ❌ Don't164- DO NOT use ARIA attributes where native HTML semantics already communicate the role. A `<button>` does not need `role="button"`. A `<nav>` does not need `role="navigation"`. Adding redundant ARIA is a code smell that suggests the developer doesn't understand when ARIA is needed.165- DO NOT rely solely on color to communicate state or information. Red text for errors needs an icon or a label too. Green for success needs a checkmark or the word "Success."166- DO NOT trap keyboard focus inside a component unless it's a modal dialog (and even then, return focus to the trigger when the modal closes).167- DO NOT use `tabindex` values greater than 0. This overrides the natural DOM order and creates unpredictable tab sequences. Use `tabindex="0"` to make elements focusable in DOM order, or `tabindex="-1"` for programmatic focus only.168- DO NOT disable browser zoom with `user-scalable=no` or `maximum-scale=1`. Users with low vision depend on zoom.169- DO NOT hide focus rings with `outline: none` or `outline: 0` without providing a visible alternative. The default browser focus ring is better than no focus ring. If you style custom focus rings, ensure they have at least 3:1 contrast.170- DO NOT use `aria-label` on elements that already have visible text content. The aria-label overrides the visible text for screen readers, creating a disconnect between what sighted and non-sighted users perceive.171- DO NOT auto-play audio, video, or animation. Users with cognitive disabilities, vestibular disorders, or who are in screen-reader mode are disrupted by unexpected media.172- DO NOT use `title` attributes as the primary accessible name for interactive elements. Title tooltips are inconsistent across browsers and not accessible via keyboard.173174175## Output Format1761771. Semantic HTML with ARIA attributes only where necessary.1782. Inline comments explaining accessibility decisions (e.g., `<!-- aria-live region for async validation messages -->`).1793. A brief testing note listing what to verify manually (keyboard flow, screen reader announcement, contrast).180181## Dependencies182183- `designer/designing-ui-system/SKILL.md` — color tokens must meet contrast requirements.184- `../../frontend/building-components/SKILL.md` — component implementation must follow these accessibility patterns (cross-agent contract).