When this skill is activated, always start your first response with the 🧢 emoji.
Accessibility & WCAG
A production-grade skill for building inclusive web experiences. It encodes WCAG 2.2 standards, ARIA authoring practices, keyboard interaction patterns, and screen reader testing guidance into actionable rules and working code. Accessibility is not a checkbox - it is the baseline quality bar. Every user deserves a working product, regardless of how they interact with it.
When to use this skill
Trigger this skill when the user:
- Asks to make a component or page accessible or "a11y compliant"
- Needs to add ARIA roles, states, or properties to custom widgets
- Wants keyboard navigation implemented for interactive components
- Asks about screen reader support, announcements, or live regions
- Needs a WCAG 2.2 audit or compliance review
- Is working on focus management (modals, SPAs, route changes)
- Asks about color contrast, alt text, semantic HTML, or form labeling
- Is building custom widgets (dialog, tabs, combobox, menu, tooltip)
Do NOT trigger this skill for:
- Pure backend code with no HTML output or DOM interaction
- CSS-only styling questions that have no accessibility implications
Key principles
-
Semantic HTML first - The single highest-leverage accessibility action is using the right HTML element.
<button>gives you keyboard support, focus, activation, and screen reader announcement for free. No ARIA patch matches it. -
ARIA is a last resort - ARIA fills gaps where native HTML falls short. Before adding an ARIA attribute, ask: "is there a native element that does this?" If yes, use that element instead. Bad ARIA is worse than no ARIA.
-
Keyboard accessible everything - If a sighted mouse user can do something, a keyboard-only user must be able to do the same thing. There are no exceptions in WCAG 2.1 AA. Test every interaction without a mouse.
-
Test with real assistive technology - Automated tools catch approximately 30% of WCAG failures. The remaining 70% - focus management correctness, announcement quality, logical reading order, cognitive load - requires manual testing with VoiceOver, NVDA, or real users with disabilities.
-
Accessibility is not optional - It is a legal requirement (ADA, Section 508, EN 301 549), a quality signal, and the right thing to do. Build it in from the start; retrofitting is ten times harder than doing it correctly the first time.
Core concepts
POUR Principles (WCAG foundation)
Every WCAG criterion maps to one of four properties:
| Principle | Definition | Examples |
|---|---|---|
| Perceivable | Info must be presentable to users in ways they can perceive | Alt text, captions, sufficient contrast, adaptable layout |
| Operable | UI must be operable by all users | Keyboard access, no seizure-triggering content, enough time |
| Understandable | Info and UI must be understandable | Clear labels, consistent navigation, error identification |
| Robust | Content must be robust enough for AT to parse | Valid HTML, ARIA used correctly, name/role/value exposed |
WCAG Conformance Levels
| Level | Meaning | Target |
|---|---|---|
| A | Removes major barriers | Legal floor in most jurisdictions |
| AA | Removes most barriers | Industry standard; required by ADA, EN 301 549, AODA |
| AAA | Enhanced, specialized needs | Aspirational; not required for full sites |
Target AA. New WCAG 2.2 AA criteria: focus appearance (2.4.11), dragging alternative (2.5.7), minimum target size 24x24px (2.5.8).
ARIA Roles, States, and Properties
ARIA exposes semantics to the accessibility tree - it does not change visual rendering or add keyboard behavior. Three categories:
- Roles - What the element is:
role="dialog",role="tab",role="alert" - States - Dynamic condition:
aria-expanded,aria-selected,aria-disabled,aria-invalid - Properties - Stable relationships:
aria-label,aria-labelledby,aria-describedby,aria-controls
The Five Rules of ARIA:
- Don't use ARIA if a native HTML element exists
- Don't change native semantics unless absolutely necessary
- All interactive ARIA controls must be keyboard operable
- Don't apply
aria-hidden="true"to focusable elements - All interactive elements must have an accessible name
Focus Management Model
- Tab order follows DOM order - keep DOM order logical and matching visual order
tabindex="0"- adds element to natural tab ordertabindex="-1"- programmatically focusable but removed from tab sequencetabindex="1+"- avoid; creates unpredictable tab order- Roving tabindex - composite widgets (tabs, toolbars, radio groups): only one item in tab order at a time; arrow keys navigate within
- Focus trap - modal dialogs must trap Tab/Shift+Tab within the dialog
- Focus return - always return focus to the trigger element when a modal or overlay closes
Common tasks
1. Write semantic HTML for common patterns
Choose elements for meaning, not appearance. Native semantics are free accessibility.
<!-- Page structure -->
<header>
<nav aria-label="Primary navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>
<main id="main-content" tabindex="-1">
<h1>Page Title</h1>
<article>
<h2>Article heading</h2>
<p>Content...</p>
</article>
<aside aria-label="Related links">...</aside>
</main>
<footer>
<nav aria-label="Footer navigation">...</nav>
</footer>
<!-- Skip link - must be first focusable element -->
<a href="#main-content" class="skip-link">Skip to main content</a>
.skip-link {
position: absolute;
top: -100%;
left: 0;
background: #005fcc;
color: #fff;
padding: 0.5rem 1rem;
z-index: 9999;
}
.skip-link:focus {
top: 0;
}
2. Implement keyboard navigation for custom widgets
Roving tabindex for a toolbar/tab list - only one item in tab order at a time:
function Toolbar({ items }: { items: { id: string; label: string }[] }) {
const [activeIndex, setActiveIndex] = React.useState(0);
const refs = React.useRef<(HTMLButtonElement | null)[]>([]);
const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
let next = index;
if (e.key === 'ArrowRight') next = (index + 1) % items.length;
else if (e.key === 'ArrowLeft') next = (index - 1 + items.length) % items.length;
else if (e.key === 'Home') next = 0;
else if (e.key === 'End') next = items.length - 1;
else return;
e.preventDefault();
setActiveIndex(next);
refs.current[next]?.focus();
};
return (
<div role="toolbar" aria-label="Text formatting">
{items.map((item, i) => (
<button
key={item.id}
ref={(el) => { refs.current[i] = el; }}
tabIndex={i === activeIndex ? 0 : -1}
onKeyDown={(e) => handleKeyDown(e, i)}
onClick={() => setActiveIndex(i)}
>
{item.label}
</button>
))}
</div>
);
}
3. Add ARIA to interactive components
For detailed accessible Dialog (Modal) and Tabs implementations with focus trapping, roving tabindex, and correct ARIA roles/states, see references/widget-examples.md.
4. Ensure color contrast compliance
WCAG AA contrast requirements:
| Element | Minimum ratio |
|---|---|
| Normal text (< 18pt / < 14pt bold) | 4.5:1 |
| Large text (>= 18pt / >= 14pt bold) | 3:1 |
| UI components (input borders, icons) | 3:1 |
| Focus indicators | 3:1 against adjacent color |
/* Focus ring - must meet 3:1 against neighboring colors */
:focus-visible {
outline: 3px solid #005fcc;
outline-offset: 2px;
border-radius: 2px;
}
/* Never convey information by color alone */
.field-error {
color: #c0392b; /* red - supplementary only */
display: flex;
align-items: center;
gap: 0.25rem;
}
/* The icon + text label carry the meaning; colo