Ensuring Accessibility
Provides WCAG 2.1 AA compliance patterns for building inclusive frontend interfaces with proper semantic markup, keyboard navigation, focus management, and screen reader support.
Accessibility Implementation Process
Use the checklist below and track your progress:
Progress:
- [ ] Step 1: Choose semantic elements
- [ ] Step 2: Implement keyboard navigation
- [ ] Step 3: Add ARIA where HTML falls short
- [ ] Step 4: Verify color and contrast
- [ ] Step 5: Test with assistive technology
Step 1: Choose semantic elements
Select the correct HTML element for each piece of UI:
- Interactive elements:
<button> — actions (submit, toggle, open menu)
<a href> — navigation to another page or location
<input>, <select>, <textarea> — form data entry
- Landmarks:
<header> — page or section header
<nav> — navigation region
<main> — primary content (one per page)
<aside> — tangentially related content
<footer> — page or section footer
- Structure:
<article> — self-contained composition
<section> — thematic grouping (must have a heading)
<details> / <summary> — native expand/collapse
- Heading hierarchy:
- One
<h1> per page
- Logical nesting:
h2 → h3 → h4
- Never skip levels (e.g.,
h2 → h4)
- Lists:
<ul> / <ol> for collections of items
<dl> for key-value pairs (definition lists)
If the component library wraps these elements, verify the rendered HTML output matches expectations using browser devtools.
Step 2: Implement keyboard navigation
All interactive elements must be focusable. Native interactive elements (<button>, <a>, <input>) are focusable by default. Custom interactive elements need tabindex="0".
Tab order must follow visual and logical reading order. Avoid tabindex values greater than 0 — they create unpredictable focus sequences.
Custom interactive components need explicit keyboard handlers:
| Component |
Keyboard behavior |
| Button |
Enter + Space to activate |
| Menu |
Arrow keys to navigate items, Escape to close, Enter to select |
| Tabs |
Left/Right arrows to switch tabs, Tab to leave the tab group |
| Dialog / Modal |
Tab trapped inside, Escape to close |
| Accordion |
Enter/Space to expand/collapse, Arrow keys between headers |
| Combobox |
Arrow keys to navigate options, Enter to select, Escape to close |
Focus visibility — never remove the focus outline (outline: none) without providing a visible replacement. Custom focus styles must have at least 3:1 contrast against adjacent colors.
Programmatic focus management — move focus when context changes:
- Modal opens → focus the first focusable element inside
- Modal closes → return focus to the element that triggered it
- Route change → focus the new page heading or main content
- Dynamic content added → focus the new content or announce it via
aria-live
Step 3: Add ARIA where HTML falls short
Rule: prefer native HTML semantics. Only add ARIA when HTML cannot express the pattern.
Common patterns requiring ARIA:
- Icon-only buttons: Add
aria-label="Close" (or the appropriate action description).
- Expandable sections:
aria-expanded="true" or aria-expanded="false" on the trigger element.
- Live updates:
aria-live="polite" for non-urgent updates (data refreshed, filter applied). aria-live="assertive" for urgent updates (session expiring, critical error).
- Dialogs:
aria-modal="true", aria-labelledby pointing to the dialog title, aria-describedby pointing to the dialog description.
- Form errors:
aria-invalid="true" on the invalid field
aria-describedby pointing to the error message element
- Error container with
role="alert" or aria-live="assertive" for immediate announcement
- Loading states: Container with
role="status" for the loading message (e.g., "Loading results..."). Note: role="status" implicitly sets aria-live="polite" — no need to add both.
- Current page in navigation:
aria-current="page" on the active nav link.
- Progress indicators:
role="progressbar" with aria-valuenow, aria-valuemin, aria-valuemax.
Never do:
role="button" on a <div> — use <button> instead
- ARIA that duplicates native semantics (e.g.,
role="heading" on an <h2>)
aria-label on non-interactive, non-landmark elements (screen readers may ignore it)
Step 4: Verify color and contrast
| Element |
Minimum contrast ratio |
Examples |
| Normal text (< 24px) |
4.5:1 |
Body text, labels, captions, small links |
| Large text (≥ 24px / 18pt, or ≥ 19px / 14pt bold) |
3:1 |
Headings, large labels, prominent links |
| Interactive component boundaries |
3:1 |
Button borders, input outlines, toggle tracks |
| Non-text content conveying information |
3:1 |
Status icons, chart segments, badges |
Additional rules:
- Never convey information through color alone. Error states need an icon or text label in addition to color. Status indicators need a text label, not just a colored dot.
- Ensure focus indicators meet 3:1 contrast against the background.
- Test both light and dark themes if the application supports them.
- Verify disabled states are visually distinguishable but don't need to meet contrast minimums (WCAG exempts disabled controls).
Step 5: Test with assistive technology
Run through these verification steps:
Keyboard walkthrough:
- Tab through the entire page. Is every interactive element reachable?
- Is the focus order logical (matches visual layout)?
- Is focus always visible?
- Can you dismiss overlays with Escape? Navigate menus with arrows?
Screen reader verification:
- Do headings, landmarks, buttons, links, and form fields announce correctly?
- Are images described (alt text) or hidden (
alt="" for decorative)?
- Do dynamic changes announce via live regions?
- Do form errors announce when they appear?
Zoom and reflow:
- At 200% zoom, is text resizable without loss of content or functionality? (SC 1.4.4)
- At 400% zoom (320px viewport width), does content reflow without horizontal scrolling? (SC 1.4.10)
- Are touch targets generously sized? (44×44 CSS pixels recommended as best practice — not a WCAG 2.1 AA requirement)
Accessibility tree inspection:
- Open browser devtools → Accessibility tab.
- Verify the semantic structure matches the intended component roles.
- Check that ARIA attributes are correctly applied and not conflicting.
Automated accessibility testing (agent-actionable):
- Run axe-core CLI against the page:
npx @axe-core/cli <URL>.
- If the URL is not known, ask the user for the URL before running.
- Parse the output and group violations by impact level (critical, serious, moderate, minor).
- For each violation, report: rule ID, impact, affected elements, and recommended fix.
- Re-run after fixes to confirm violations are resolved.
ARIA Usage Quick Reference
| Need |
HTML Solution |
ARIA Fallback (only if HTML insufficient) |
| Button |
<button> |
role="button" + tabindex="0" + key handlers |
| Link |
<a href> |
role="link" (rare) |
| Navigation region |
<nav> |
role="navigation" |
| Main content |
<main> |
role="main" |
| Dialog |
<dialog> |
role="dialog" + aria-modal="true" |
| Live update |
— |
aria-live="polite" on container |
| Expand/collapse |
<details> / <summary> |
aria-expanded on trigger |
| Icon-only button |
— |
aria-label on the button |
| Form error |
— |
aria-invalid + aria-describedby |
| Current page |
— |
aria-current="page" on nav link |
| Progress |
<progress> |
role="progressbar" + aria-valuenow |
Contrast Requirements
| Element |
Minimum ratio |
Example |
| Normal text (< 24px) |
4.5:1 |
Body text, labels, captions |
| Large text (≥ 24px / 18pt, or ≥ 19px / 14pt bold) |
3:1 |
Headings, large labels |
| Interactive component boundaries |
3:1 |
Button borders, input outlines |
| Non-text (icons conveying info) |
3:1 |
Status icons, chart segments |
Accessibility Checklist
Accessibility:
- [ ] Semantic HTML elements used (button, nav, main, etc.)
- [ ] One h1 per page, logical heading hierarchy (no skipped levels)
- [ ] All interactive elements keyboard-navigable
- [ ] Focus visible on every interactive element
- [ ] Tab order follows visual/logical layout
- [ ] Modal/dialog traps focus and returns on close
- [ ] Icon-only buttons have aria-label
- [ ] Form fields have visible labels (not placeholder-only)
- [ ] Form errors announced to screen readers (role="alert" or aria-live)
- [ ] Error states use icon/text in addition to color
- [ ] Color contrast meets 4.5:1 (normal text) / 3:1 (large text)
- [ ] ARIA landmarks present (header, main, nav, footer)
- [ ] Text resizable to 200% without loss of content (SC 1.4.4)
- [ ] Content reflows at 400% zoom / 320px width without horizontal scroll (SC 1.4.10)
RTL / Bidirectional Text Support
| Rule |
Description |
| Use logical CSS properties |
margin-inline-start instead of margin-left; padding-inline-end instead of padding-right |
| Let layout engine handle direction |
Set dir="rtl" on root; avoid manual transforms for standard layout |
| Icons may need flipping |
Directional icons (arrows, progress bars) may need transform: scaleX(-1) in RTL |
| Test both directions |
Verify layout, alignment, and text truncation in both LTR and RTL |
Connected Skills
tsh-implementing-frontend — for component composition patterns that support accessible structure
tsh-implementing-forms — for accessible form field patterns, labels, and error announcements
tsh-reviewing-frontend — for accessibility spot-checks during code review
tsh-optimizing-frontend — for performance optimizations that also impact accessibility (loading speed, interaction responsiveness)
1---2name: tsh-ensuring-accessibility3description: WCAG 2.1 AA compliance, semantic HTML, ARIA patterns, keyboard navigation, focus management, screen reader support, and color contrast requirements. Use when implementing accessible components, auditing UI for accessibility issues, reviewing frontend code for a11y compliance, or building inclusive forms and interactive widgets.4---56# Ensuring Accessibility78Provides WCAG 2.1 AA compliance patterns for building inclusive frontend interfaces with proper semantic markup, keyboard navigation, focus management, and screen reader support.910<principles>1112<semantic-html-first>13Start with the correct HTML element. `<button>` for actions, `<a>` for navigation, `<nav>` for navigation regions, `<main>` for primary content. Native semantics are free, reliable, and require zero ARIA. Only reach for ARIA when HTML alone cannot convey the meaning.14</semantic-html-first>1516<keyboard-is-mandatory>17Every interaction available to a mouse user must be available to a keyboard user. Tab, Escape, Enter, Space, Arrow keys — these are the vocabulary of keyboard interaction. Missing keyboard support is not a minor issue — it is a blocker for many users.18</keyboard-is-mandatory>1920<never-color-alone>21Color must never be the sole means of conveying information. Error states need icons + text, not just red borders. Status indicators need labels, not just colored dots. Always pair visual indicators with non-visual alternatives.22</never-color-alone>2324</principles>2526## Accessibility Implementation Process2728Use the checklist below and track your progress:2930```31Progress:32- [ ] Step 1: Choose semantic elements33- [ ] Step 2: Implement keyboard navigation34- [ ] Step 3: Add ARIA where HTML falls short35- [ ] Step 4: Verify color and contrast36- [ ] Step 5: Test with assistive technology37```3839**Step 1: Choose semantic elements**4041Select the correct HTML element for each piece of UI:4243- **Interactive elements**:44 - `<button>` — actions (submit, toggle, open menu)45 - `<a href>` — navigation to another page or location46 - `<input>`, `<select>`, `<textarea>` — form data entry47- **Landmarks**:48 - `<header>` — page or section header49 - `<nav>` — navigation region50 - `<main>` — primary content (one per page)51 - `<aside>` — tangentially related content52 - `<footer>` — page or section footer53- **Structure**:54 - `<article>` — self-contained composition55 - `<section>` — thematic grouping (must have a heading)56 - `<details>` / `<summary>` — native expand/collapse57- **Heading hierarchy**:58 - One `<h1>` per page59 - Logical nesting: `h2` → `h3` → `h4`60 - Never skip levels (e.g., `h2` → `h4`)61- **Lists**:62 - `<ul>` / `<ol>` for collections of items63 - `<dl>` for key-value pairs (definition lists)6465If the component library wraps these elements, verify the rendered HTML output matches expectations using browser devtools.6667**Step 2: Implement keyboard navigation**6869All interactive elements must be focusable. Native interactive elements (`<button>`, `<a>`, `<input>`) are focusable by default. Custom interactive elements need `tabindex="0"`.7071- **Tab order** must follow visual and logical reading order. Avoid `tabindex` values greater than 0 — they create unpredictable focus sequences.72- **Custom interactive components** need explicit keyboard handlers:7374 | Component | Keyboard behavior |75 | -------------- | ---------------------------------------------------------------- |76 | Button | Enter + Space to activate |77 | Menu | Arrow keys to navigate items, Escape to close, Enter to select |78 | Tabs | Left/Right arrows to switch tabs, Tab to leave the tab group |79 | Dialog / Modal | Tab trapped inside, Escape to close |80 | Accordion | Enter/Space to expand/collapse, Arrow keys between headers |81 | Combobox | Arrow keys to navigate options, Enter to select, Escape to close |8283- **Focus visibility** — never remove the focus outline (`outline: none`) without providing a visible replacement. Custom focus styles must have at least 3:1 contrast against adjacent colors.84- **Programmatic focus management** — move focus when context changes:85 - Modal opens → focus the first focusable element inside86 - Modal closes → return focus to the element that triggered it87 - Route change → focus the new page heading or main content88 - Dynamic content added → focus the new content or announce it via `aria-live`8990**Step 3: Add ARIA where HTML falls short**9192Rule: prefer native HTML semantics. Only add ARIA when HTML cannot express the pattern.9394Common patterns requiring ARIA:9596- **Icon-only buttons**: Add `aria-label="Close"` (or the appropriate action description).97- **Expandable sections**: `aria-expanded="true"` or `aria-expanded="false"` on the trigger element.98- **Live updates**: `aria-live="polite"` for non-urgent updates (data refreshed, filter applied). `aria-live="assertive"` for urgent updates (session expiring, critical error).99- **Dialogs**: `aria-modal="true"`, `aria-labelledby` pointing to the dialog title, `aria-describedby` pointing to the dialog description.100- **Form errors**:101 - `aria-invalid="true"` on the invalid field102 - `aria-describedby` pointing to the error message element103 - Error container with `role="alert"` or `aria-live="assertive"` for immediate announcement104- **Loading states**: Container with `role="status"` for the loading message (e.g., "Loading results..."). Note: `role="status"` implicitly sets `aria-live="polite"` — no need to add both.105- **Current page in navigation**: `aria-current="page"` on the active nav link.106- **Progress indicators**: `role="progressbar"` with `aria-valuenow`, `aria-valuemin`, `aria-valuemax`.107108Never do:109110- `role="button"` on a `<div>` — use `<button>` instead111- ARIA that duplicates native semantics (e.g., `role="heading"` on an `<h2>`)112- `aria-label` on non-interactive, non-landmark elements (screen readers may ignore it)113114**Step 4: Verify color and contrast**115116| Element | Minimum contrast ratio | Examples |117| ------------------------------------------------- | ---------------------- | --------------------------------------------- |118| Normal text (< 24px) | 4.5:1 | Body text, labels, captions, small links |119| Large text (≥ 24px / 18pt, or ≥ 19px / 14pt bold) | 3:1 | Headings, large labels, prominent links |120| Interactive component boundaries | 3:1 | Button borders, input outlines, toggle tracks |121| Non-text content conveying information | 3:1 | Status icons, chart segments, badges |122123Additional rules:124125- Never convey information through color alone. Error states need an icon or text label in addition to color. Status indicators need a text label, not just a colored dot.126- Ensure focus indicators meet 3:1 contrast against the background.127- Test both light and dark themes if the application supports them.128- Verify disabled states are visually distinguishable but don't need to meet contrast minimums (WCAG exempts disabled controls).129130**Step 5: Test with assistive technology**131132Run through these verification steps:1331341. **Keyboard walkthrough**:135 - Tab through the entire page. Is every interactive element reachable?136 - Is the focus order logical (matches visual layout)?137 - Is focus always visible?138 - Can you dismiss overlays with Escape? Navigate menus with arrows?1391402. **Screen reader verification**:141 - Do headings, landmarks, buttons, links, and form fields announce correctly?142 - Are images described (alt text) or hidden (`alt=""` for decorative)?143 - Do dynamic changes announce via live regions?144 - Do form errors announce when they appear?1451463. **Zoom and reflow**:147 - At 200% zoom, is text resizable without loss of content or functionality? (SC 1.4.4)148 - At 400% zoom (320px viewport width), does content reflow without horizontal scrolling? (SC 1.4.10)149 - Are touch targets generously sized? (44×44 CSS pixels recommended as best practice — not a WCAG 2.1 AA requirement)1501514. **Accessibility tree inspection**:152 - Open browser devtools → Accessibility tab.153 - Verify the semantic structure matches the intended component roles.154 - Check that ARIA attributes are correctly applied and not conflicting.1551565. **Automated accessibility testing** (agent-actionable):157 - Run axe-core CLI against the page: `npx @axe-core/cli <URL>`.158 - If the URL is not known, ask the user for the URL before running.159 - Parse the output and group violations by impact level (critical, serious, moderate, minor).160 - For each violation, report: rule ID, impact, affected elements, and recommended fix.161 - Re-run after fixes to confirm violations are resolved.162163## ARIA Usage Quick Reference164165| Need | HTML Solution | ARIA Fallback (only if HTML insufficient) |166| ----------------- | ------------------------- | ----------------------------------------------- |167| Button | `<button>` | `role="button"` + `tabindex="0"` + key handlers |168| Link | `<a href>` | `role="link"` (rare) |169| Navigation region | `<nav>` | `role="navigation"` |170| Main content | `<main>` | `role="main"` |171| Dialog | `<dialog>` | `role="dialog"` + `aria-modal="true"` |172| Live update | — | `aria-live="polite"` on container |173| Expand/collapse | `<details>` / `<summary>` | `aria-expanded` on trigger |174| Icon-only button | — | `aria-label` on the button |175| Form error | — | `aria-invalid` + `aria-describedby` |176| Current page | — | `aria-current="page"` on nav link |177| Progress | `<progress>` | `role="progressbar"` + `aria-valuenow` |178179## Contrast Requirements180181| Element | Minimum ratio | Example |182| ------------------------------------------------- | ------------- | ------------------------------ |183| Normal text (< 24px) | 4.5:1 | Body text, labels, captions |184| Large text (≥ 24px / 18pt, or ≥ 19px / 14pt bold) | 3:1 | Headings, large labels |185| Interactive component boundaries | 3:1 | Button borders, input outlines |186| Non-text (icons conveying info) | 3:1 | Status icons, chart segments |187188## Accessibility Checklist189190```191Accessibility:192- [ ] Semantic HTML elements used (button, nav, main, etc.)193- [ ] One h1 per page, logical heading hierarchy (no skipped levels)194- [ ] All interactive elements keyboard-navigable195- [ ] Focus visible on every interactive element196- [ ] Tab order follows visual/logical layout197- [ ] Modal/dialog traps focus and returns on close198- [ ] Icon-only buttons have aria-label199- [ ] Form fields have visible labels (not placeholder-only)200- [ ] Form errors announced to screen readers (role="alert" or aria-live)201- [ ] Error states use icon/text in addition to color202- [ ] Color contrast meets 4.5:1 (normal text) / 3:1 (large text)203- [ ] ARIA landmarks present (header, main, nav, footer)204- [ ] Text resizable to 200% without loss of content (SC 1.4.4)205- [ ] Content reflows at 400% zoom / 320px width without horizontal scroll (SC 1.4.10)206```207208## RTL / Bidirectional Text Support209210| Rule | Description |211| ---------------------------------- | ----------------------------------------------------------------------------------------------- |212| Use logical CSS properties | `margin-inline-start` instead of `margin-left`; `padding-inline-end` instead of `padding-right` |213| Let layout engine handle direction | Set `dir="rtl"` on root; avoid manual transforms for standard layout |214| Icons may need flipping | Directional icons (arrows, progress bars) may need `transform: scaleX(-1)` in RTL |215| Test both directions | Verify layout, alignment, and text truncation in both LTR and RTL |216217## Connected Skills218219- `tsh-implementing-frontend` — for component composition patterns that support accessible structure220- `tsh-implementing-forms` — for accessible form field patterns, labels, and error announcements221- `tsh-reviewing-frontend` — for accessibility spot-checks during code review222- `tsh-optimizing-frontend` — for performance optimizations that also impact accessibility (loading speed, interaction responsiveness)