accessibility
Build WCAG 2.1 AA compliant websites with semantic HTML, proper ARIA, focus management, and screen reader support. Includes color contrast (4.5:1 text), keyboard navigation, form labels, and live regions. Use when implementing accessible interfaces, fixing screen reader issues, keyboard navigation, or troubleshooting "focus outline missing", "aria-label required", "insufficient contrast".
What this skill does
# Web Accessibility (WCAG 2.1 AA)
**Status**: Production Ready ✅
**Last Updated**: 2026-01-14
**Dependencies**: None (framework-agnostic)
**Standards**: WCAG 2.1 Level AA
---
## Quick Start (5 Minutes)
### 1. Semantic HTML Foundation
Choose the right element - don't use `div` for everything:
```html
<!-- ❌ WRONG - divs with onClick -->
<div onclick="submit()">Submit</div>
<div onclick="navigate()">Next page</div>
<!-- ✅ CORRECT - semantic elements -->
<button type="submit">Submit</button>
<a href="/next">Next page</a>
```
**Why this matters:**
- Semantic elements have built-in keyboard support
- Screen readers announce role automatically
- Browser provides default accessible behaviors
### 2. Focus Management
Make interactive elements keyboard-accessible:
```css
/* ❌ WRONG - removes focus outline */
button:focus { outline: none; }
/* ✅ CORRECT - custom accessible outline */
button:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
```
**CRITICAL:**
- Never remove focus outlines without replacement
- Use `:focus-visible` to show only on keyboard focus
- Ensure 3:1 contrast ratio for focus indicators
### 3. Text Alternatives
Every non-text element needs a text alternative:
```html
<!-- ❌ WRONG - no alt text -->
<img src="logo.png">
<button><svg>...</svg></button>
<!-- ✅ CORRECT - proper alternatives -->
<img src="logo.png" alt="Company Name">
<button aria-label="Close dialog"><svg>...</svg></button>
```
---
## The 5-Step Accessibility Process
### Step 1: Choose Semantic HTML
**Decision tree for element selection:**
```
Need clickable element?
├─ Navigates to another page? → <a href="...">
├─ Submits form? → <button type="submit">
├─ Opens dialog? → <button aria-haspopup="dialog">
└─ Other action? → <button type="button">
Grouping content?
├─ Self-contained article? → <article>
├─ Thematic section? → <section>
├─ Navigation links? → <nav>
└─ Supplementary info? → <aside>
Form element?
├─ Text input? → <input type="text">
├─ Multiple choice? → <select> or <input type="radio">
├─ Toggle? → <input type="checkbox"> or <button aria-pressed>
└─ Long text? → <textarea>
```
**See `references/semantic-html.md` for complete guide.**
### Step 2: Add ARIA When Needed
**Golden rule: Use ARIA only when HTML can't express the pattern.**
```html
<!-- ❌ WRONG - unnecessary ARIA -->
<button role="button">Click me</button> <!-- Button already has role -->
<!-- ✅ CORRECT - ARIA fills semantic gap -->
<div role="dialog" aria-labelledby="title" aria-modal="true">
<h2 id="title">Confirm action</h2>
<!-- No HTML dialog yet, so role needed -->
</div>
<!-- ✅ BETTER - Use native HTML when available -->
<dialog aria-labelledby="title">
<h2 id="title">Confirm action</h2>
</dialog>
```
**Common ARIA patterns:**
- `aria-label` - When visible label doesn't exist
- `aria-labelledby` - Reference existing text as label
- `aria-describedby` - Additional description
- `aria-live` - Announce dynamic updates
- `aria-expanded` - Collapsible/expandable state
**See `references/aria-patterns.md` for complete patterns.**
### Step 3: Implement Keyboard Navigation
**All interactive elements must be keyboard-accessible:**
```typescript
// Tab order management
function Dialog({ onClose }) {
const dialogRef = useRef<HTMLDivElement>(null);
const previousFocus = useRef<HTMLElement | null>(null);
useEffect(() => {
// Save previous focus
previousFocus.current = document.activeElement as HTMLElement;
// Focus first element in dialog
const firstFocusable = dialogRef.current?.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
(firstFocusable as HTMLElement)?.focus();
// Trap focus within dialog
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
if (e.key === 'Tab') {
// Focus trap logic here
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
// Restore focus on close
previousFocus.current?.focus();
};
}, [onClose]);
return <div ref={dialogRef} role="dialog">...</div>;
}
```
**Essential keyboard patterns:**
- Tab/Shift+Tab: Navigate between focusable elements
- Enter/Space: Activate buttons/links
- Arrow keys: Navigate within components (tabs, menus)
- Escape: Close dialogs/menus
- Home/End: Jump to first/last item
**See `references/focus-management.md` for complete patterns.**
### Step 4: Ensure Color Contrast
**WCAG AA requirements:**
- Normal text (under 18pt): 4.5:1 contrast ratio
- Large text (18pt+ or 14pt+ bold): 3:1 contrast ratio
- UI components (buttons, borders): 3:1 contrast ratio
```css
/* ❌ WRONG - insufficient contrast */
:root {
--background: #ffffff;
--text: #999999; /* 2.8:1 - fails WCAG AA */
}
/* ✅ CORRECT - sufficient contrast */
:root {
--background: #ffffff;
--text: #595959; /* 4.6:1 - passes WCAG AA */
}
```
**Testing tools:**
- Browser DevTools (Chrome/Firefox have built-in checkers)
- Contrast checker extensions
- axe DevTools extension
**See `references/color-contrast.md` for complete guide.**
### Step 5: Make Forms Accessible
**Every form input needs a visible label:**
```html
<!-- ❌ WRONG - placeholder is not a label -->
<input type="email" placeholder="Email address">
<!-- ✅ CORRECT - proper label -->
<label for="email">Email address</label>
<input type="email" id="email" name="email" required aria-required="true">
```
**Error handling:**
```html
<label for="email">Email address</label>
<input
type="email"
id="email"
name="email"
aria-invalid="true"
aria-describedby="email-error"
>
<span id="email-error" role="alert">
Please enter a valid email address
</span>
```
**Live regions for dynamic errors:**
```html
<div role="alert" aria-live="assertive" aria-atomic="true">
Form submission failed. Please fix the errors above.
</div>
```
**See `references/forms-validation.md` for complete patterns.**
---
## Critical Rules
### Always Do
✅ Use semantic HTML elements first (button, a, nav, article, etc.)
✅ Provide text alternatives for all non-text content
✅ Ensure 4.5:1 contrast for normal text, 3:1 for large text/UI
✅ Make all functionality keyboard accessible
✅ Test with keyboard only (unplug mouse)
✅ Test with screen reader (NVDA on Windows, VoiceOver on Mac)
✅ Use proper heading hierarchy (h1 → h2 → h3, no skipping)
✅ Label all form inputs with visible labels
✅ Provide focus indicators (never just `outline: none`)
✅ Use `aria-live` for dynamic content updates
### Never Do
❌ Use `div` with `onClick` instead of `button`
❌ Remove focus outlines without replacement
❌ Use color alone to convey information
❌ Use placeholders as labels
❌ Skip heading levels (h1 → h3)
❌ Use `tabindex` > 0 (messes with natural order)
❌ Add ARIA when semantic HTML exists
❌ Forget to restore focus after closing dialogs
❌ Use `role="presentation"` on focusable elements
❌ Create keyboard traps (no way to escape)
---
## Known Issues Prevention
This skill prevents **12** documented accessibility issues:
### Issue #1: Missing Focus Indicators
**Error**: Interactive elements have no visible focus indicator
**Source**: WCAG 2.4.7 (Focus Visible)
**Why It Happens**: CSS reset removes default outline
**Prevention**: Always provide custom focus-visible styles
### Issue #2: Insufficient Color Contrast
**Error**: Text has less than 4.5:1 contrast ratio
**Source**: WCAG 1.4.3 (Contrast Minimum)
**Why It Happens**: Using light gray text on white background
**Prevention**: Test all text colors with contrast checker
### Issue #3: Missing Alt Text
**Error**: Images missing alt attributes
**Source**: WCAG 1.1.1 (Non-text Content)
**Why It Happens**: Forgot to add or thought it was optional
**Prevention**: Add alt="" for decorative, descriptive alt for meaningful images
### Issue #4: Keyboard Navigation Broken
**Error**: Interactive elements not reachable by Related in Web Dev
generating-lwc-components
IncludedLightning Web Components with PICKLES methodology and 165-point scoring. Use this skill when the user creates or edits LWC components, builds wire service patterns, or writes Jest tests for LWC. TRIGGER when: user creates/edits LWC components, touches lwc/**/*.js, .html, .css, .js-meta.xml files, or asks about wire service, SLDS, or Jest LWC tests. DO NOT TRIGGER when: Apex classes (use generating-apex), Aura components, or Visualforce.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Set up queries with useQuery, mutations with useMutation, configure QueryClient caching strategies, implement optimistic updates, and handle infinite scroll with useInfiniteQuery. Use when: setting up data fetching in React projects, migrating from v4 to v5, or fixing object syntax required errors, query callbacks removed issues, cacheTime renamed to gcTime, isPending vs isLoading confusion, keepPreviousData removed problems.
document-processor-api
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
nutrient-document-processing
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Covers useMutationState, simplified optimistic updates, throwOnError, network mode (offline/PWA), and infiniteQueryOptions. Use when setting up data fetching, fixing v4→v5 migration errors (object syntax, gcTime, isPending, keepPreviousData), or debugging SSR/hydration issues with streaming server components.
accelint-nextjs-best-practices
IncludedNext.js performance optimization and best practices. Use when writing Next.js code (App Router or Pages Router); implementing Server Components, Server Actions, or API routes; optimizing RSC serialization, data fetching, or server-side rendering; reviewing Next.js code for performance issues; fixing authentication in Server Actions; or implementing Suspense boundaries, parallel data fetching, or request deduplication.