accessibilityBuild 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".
Install via ClawdBot CLI:
clawdbot install Veeramanikandanr48/accessibilityStatus: Production Ready ā
Last Updated: 2026-01-14
Dependencies: None (framework-agnostic)
Standards: WCAG 2.1 Level AA
Choose the right element - don't use div for everything:
<!-- ā 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:
Make interactive elements keyboard-accessible:
/* ā WRONG - removes focus outline */
button:focus { outline: none; }
/* ā
CORRECT - custom accessible outline */
button:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
CRITICAL:
:focus-visible to show only on keyboard focusEvery non-text element needs a text alternative:
<!-- ā 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>
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.
Golden rule: Use ARIA only when HTML can't express the pattern.
<!-- ā 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 existaria-labelledby - Reference existing text as labelaria-describedby - Additional descriptionaria-live - Announce dynamic updatesaria-expanded - Collapsible/expandable stateSee references/aria-patterns.md for complete patterns.
All interactive elements must be keyboard-accessible:
// 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:
See references/focus-management.md for complete patterns.
WCAG AA requirements:
/* ā 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:
See references/color-contrast.md for complete guide.
Every form input needs a visible label:
<!-- ā 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:
<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:
<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.
ā 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
ā 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)
This skill prevents 12 documented accessibility issues:
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
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
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
Error: Interactive elements not reachable by keyboard
Source: WCAG 2.1.1 (Keyboard)
Why It Happens: Using div onClick instead of button
Prevention: Use semantic interactive elements (button, a)
Error: Input fields missing associated labels
Source: WCAG 3.3.2 (Labels or Instructions)
Why It Happens: Using placeholder as label
Prevention: Always use element with for/id association
Error: Heading hierarchy jumps from h1 to h3
Source: WCAG 1.3.1 (Info and Relationships)
Why It Happens: Using headings for visual styling instead of semantics
Prevention: Use headings in order, style with CSS
Error: Tab key exits dialog to background content
Source: WCAG 2.4.3 (Focus Order)
Why It Happens: No focus trap implementation
Prevention: Implement focus trap for modal dialogs
Error: Screen reader doesn't announce updates
Source: WCAG 4.1.3 (Status Messages)
Why It Happens: Dynamic content added without announcement
Prevention: Use aria-live="polite" or "assertive"
Error: Using only color to convey status
Source: WCAG 1.4.1 (Use of Color)
Why It Happens: Red text for errors without icon/text
Prevention: Add icon + text label, not just color
Error: Links with "click here" or "read more"
Source: WCAG 2.4.4 (Link Purpose)
Why It Happens: Generic link text without context
Prevention: Use descriptive link text or aria-label
Error: Video/audio auto-plays without user control
Source: WCAG 1.4.2 (Audio Control)
Why It Happens: Autoplay attribute without controls
Prevention: Require user interaction to start media
Error: Custom select/checkbox without keyboard support
Source: WCAG 4.1.2 (Name, Role, Value)
Why It Happens: Building from divs without ARIA
Prevention: Use native elements or implement full ARIA pattern
))1. Unplug mouse or hide cursor
2. Tab through entire page
- Can you reach all interactive elements?
- Can you activate all buttons/links?
- Is focus order logical?
3. Use Enter/Space to activate
4. Use Escape to close dialogs
5. Use arrow keys in menus/tabs
NVDA (Windows - Free):
VoiceOver (Mac - Built-in):
What to test:
axe DevTools (Browser extension - highly recommended):
Lighthouse (Built into Chrome):
interface DialogProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}
function Dialog({ isOpen, onClose, title, children }: DialogProps) {
const dialogRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!isOpen) return;
const previousFocus = document.activeElement as HTMLElement;
// Focus first focusable element
const firstFocusable = dialogRef.current?.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
) as HTMLElement;
firstFocusable?.focus();
// Focus trap
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
if (e.key === 'Tab') {
const focusableElements = dialogRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (!focusableElements?.length) return;
const first = focusableElements[0] as HTMLElement;
const last = focusableElements[focusableElements.length - 1] as HTMLElement;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
previousFocus?.focus();
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<>
{/* Backdrop */}
<div
className="dialog-backdrop"
onClick={onClose}
aria-hidden="true"
/>
{/* Dialog */}
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
className="dialog"
>
<h2 id="dialog-title">{title}</h2>
<div className="dialog-content">{children}</div>
<button onClick={onClose} aria-label="Close dialog">Ć</button>
</div>
</>
);
}
When to use: Any modal dialog or overlay that blocks interaction with background content.
function Tabs({ tabs }: { tabs: Array<{ label: string; content: React.ReactNode }> }) {
const [activeIndex, setActiveIndex] = useState(0);
const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
if (e.key === 'ArrowLeft') {
e.preventDefault();
const newIndex = index === 0 ? tabs.length - 1 : index - 1;
setActiveIndex(newIndex);
} else if (e.key === 'ArrowRight') {
e.preventDefault();
const newIndex = index === tabs.length - 1 ? 0 : index + 1;
setActiveIndex(newIndex);
} else if (e.key === 'Home') {
e.preventDefault();
setActiveIndex(0);
} else if (e.key === 'End') {
e.preventDefault();
setActiveIndex(tabs.length - 1);
}
};
return (
<div>
<div role="tablist" aria-label="Content tabs">
{tabs.map((tab, index) => (
<button
key={index}
role="tab"
aria-selected={activeIndex === index}
aria-controls={`panel-${index}`}
id={`tab-${index}`}
tabIndex={activeIndex === index ? 0 : -1}
onClick={() => setActiveIndex(index)}
onKeyDown={(e) => handleKeyDown(e, index)}
>
{tab.label}
</button>
))}
</div>
{tabs.map((tab, index) => (
<div
key={index}
role="tabpanel"
id={`panel-${index}`}
aria-labelledby={`tab-${index}`}
hidden={activeIndex !== index}
tabIndex={0}
>
{tab.content}
</div>
))}
</div>
);
}
When to use: Tabbed interface with multiple panels.
<!-- Place at very top of body -->
<a href="#main-content" class="skip-link">
Skip to main content
</a>
<style>
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: var(--primary);
color: white;
padding: 8px 16px;
z-index: 9999;
}
.skip-link:focus {
top: 0;
}
</style>
<!-- Then in your layout -->
<main id="main-content" tabindex="-1">
<!-- Page content -->
</main>
When to use: All multi-page websites with navigation/header before main content.
function ContactForm() {
const [errors, setErrors] = useState<Record<string, string>>({});
const [touched, setTouched] = useState<Record<string, boolean>>({});
const validateEmail = (email: string) => {
if (!email) return 'Email is required';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return 'Email is invalid';
return '';
};
const handleBlur = (field: string, value: string) => {
setTouched(prev => ({ ...prev, [field]: true }));
const error = validateEmail(value);
setErrors(prev => ({ ...prev, [field]: error }));
};
return (
<form>
<div>
<label htmlFor="email">Email address *</label>
<input
type="email"
id="email"
name="email"
required
aria-required="true"
aria-invalid={touched.email && !!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
onBlur={(e) => handleBlur('email', e.target.value)}
/>
{touched.email && errors.email && (
<span id="email-error" role="alert" className="error">
{errors.email}
</span>
)}
</div>
<button type="submit">Submit</button>
{/* Global form error */}
<div role="alert" aria-live="assertive" aria-atomic="true">
{/* Dynamic error message appears here */}
</div>
</form>
);
}
When to use: All forms with validation.
Detailed documentation for deep dives:
When Claude should load these:
When to use: Request accessibility audit of existing page/component.
Three politeness levels:
<!-- Polite: Wait for screen reader to finish current announcement -->
<div aria-live="polite">New messages: 3</div>
<!-- Assertive: Interrupt immediately -->
<div aria-live="assertive" role="alert">
Error: Form submission failed
</div>
<!-- Off: Don't announce (default) -->
<div aria-live="off">Loading...</div>
Best practices:
polite for non-critical updates (notifications, counters)assertive for errors and critical alertsaria-atomic="true" to read entire region on changeReact Router doesn't reset focus on navigation - you need to handle it:
function App() {
const location = useLocation();
const mainRef = useRef<HTMLElement>(null);
useEffect(() => {
// Focus main content on route change
mainRef.current?.focus();
// Announce page title to screen readers
const title = document.title;
const announcement = document.createElement('div');
announcement.setAttribute('role', 'status');
announcement.setAttribute('aria-live', 'polite');
announcement.textContent = `Navigated to ${title}`;
document.body.appendChild(announcement);
setTimeout(() => announcement.remove(), 1000);
}, [location.pathname]);
return <main ref={mainRef} tabIndex={-1} id="main-content">...</main>;
}
<table>
<caption>Monthly sales by region</caption>
<thead>
<tr>
<th scope="col">Region</th>
<th scope="col">Q1</th>
<th scope="col">Q2</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">North</th>
<td>$10,000</td>
<td>$12,000</td>
</tr>
</tbody>
</table>
Key attributes:
- Describes table purposescope="col" - Identifies column headersscope="row" - Identifies row headersSymptoms: Can tab through page but don't see where focus is
Cause: CSS removed outlines or insufficient contrast
Solution:
*:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
Symptoms: Dynamic content changes but no announcement
Cause: No aria-live region
Solution: Wrap dynamic content in Symptoms: Tab key navigates to elements behind dialog Cause: No focus trap Solution: Implement focus trap (see Pattern 1 above) Symptoms: Visual errors appear but screen reader doesn't notice Cause: No aria-invalid or role="alert" Solution: Use aria-invalid + aria-describedby pointing to error message with role="alert" Use this for every page/component: Questions? Issues? Standards: WCAG 2.1 Level AA Testing Tools: axe DevTools, Lighthouse, NVDA, VoiceOver Success Criteria: 90+ Lighthouse score, 0 critical violations
Problem: Dialog focus escapes to background
Problem: Form errors not announced
Complete Setup Checklist
or appropriate language
references/wcag-checklist.md for complete requirements/a11y-auditor agent to scan your page
Generated Mar 1, 2026
An online retailer needs to update its product pages and checkout flow to meet WCAG 2.1 AA standards, ensuring all interactive elements like buttons, forms, and navigation are keyboard-accessible and screen reader-friendly. This includes fixing color contrast issues in product descriptions and adding ARIA labels to dynamic content like shopping cart updates.
A government agency requires its public service portal to be fully accessible for citizens with disabilities, focusing on semantic HTML for forms, proper focus management in multi-step processes, and live regions for status updates. This ensures compliance with legal mandates and improves usability for all users.
An e-learning platform needs to make its course interfaces accessible, implementing keyboard navigation for interactive quizzes, adding text alternatives for multimedia content, and ensuring color contrast in instructional materials. This supports diverse learners, including those using assistive technologies.
A healthcare provider's online booking system must be accessible to patients with visual or motor impairments, requiring semantic form labels, ARIA roles for appointment dialogs, and focus traps during modal interactions. This reduces barriers to accessing essential services.
A fintech company is upgrading its analytics dashboard to include accessible data visualizations, with proper ARIA descriptions for charts, keyboard shortcuts for navigation, and sufficient contrast in graphs. This enables users with disabilities to interpret financial data independently.
Offer paid audits and implementation support for businesses needing to comply with accessibility standards like WCAG, generating revenue through project-based fees or retainer contracts. This model leverages expertise in semantic HTML, ARIA, and testing tools to help clients avoid legal risks.
Develop and sell a software-as-a-service tool that automates accessibility checks and provides actionable fixes, with subscription tiers based on usage or features. Revenue comes from monthly or annual licenses, targeting web developers and design teams.
Provide online courses and certifications on web accessibility best practices, charging for enrollment or certification exams. This model monetizes educational content focused on the 5-step process, keyboard navigation, and color contrast guidelines.
š¬ Integration Tip
Start by integrating semantic HTML and focus management into existing projects, using browser DevTools for quick contrast checks and gradually adding ARIA only when necessary to avoid overcomplication.
Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
Expert frontend design guidelines for creating beautiful, modern UIs. Use when building landing pages, dashboards, or any user interface.
Use when building UI with shadcn/ui components, Tailwind CSS layouts, form patterns with react-hook-form and zod, theming, dark mode, sidebar layouts, mobile navigation, or any shadcn component question.
Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when building web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
Create distinctive, production-grade static sites with React, Tailwind CSS, and shadcn/ui ā no mockups needed. Generates bold, memorable designs from plain text requirements with anti-AI-slop aesthetics, mobile-first responsive patterns, and single-file bundling. Use when building landing pages, marketing sites, portfolios, dashboards, or any static web UI. Supports both Vite (pure static) and Next.js (Vercel deploy) workflows.
AI skill for automated UI audits. Evaluate interfaces against proven UX principles for visual hierarchy, accessibility, cognitive load, navigation, and more. Based on Making UX Decisions by Tommy Geoco.