Requirements
- Target platform
- OpenClaw
- Install method
- Manual import
- Extraction
- Extract archive
- Prerequisites
- OpenClaw
- Primary doc
- SKILL.md
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".
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".
Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.
I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Then review README.md for any prerequisites, environment setup, or post-install checks. Tell me what you changed and call out any manual steps you could not complete.
I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Then review README.md for any prerequisites, environment setup, or post-install checks. Summarize what changed and any follow-up checks I should run.
Status: 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: Semantic elements have built-in keyboard support Screen readers announce role automatically Browser provides default accessible behaviors
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: Never remove focus outlines without replacement Use :focus-visible to show only on keyboard focus Ensure 3:1 contrast ratio for focus indicators
Every 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 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.
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: 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.
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 /* β 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.
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 <label> 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
All images have alt text (or alt="" if decorative) Text contrast β₯ 4.5:1 (normal), β₯ 3:1 (large) Color not used alone to convey information Text can be resized to 200% without loss of content No auto-playing audio >3 seconds
All functionality keyboard accessible No keyboard traps Visible focus indicators Users can pause/stop/hide moving content Page titles describe purpose Focus order is logical Link purpose clear from text or context Multiple ways to find pages (menu, search, sitemap) Headings and labels describe purpose
Page language specified (<html lang="en">) Language changes marked (<span lang="es">) No unexpected context changes on focus/input Consistent navigation across site Form labels/instructions provided Input errors identified and described Error prevention for legal/financial/data changes
Valid HTML (no parsing errors) Name, role, value available for all UI components Status messages identified (aria-live)
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): Download: https://www.nvaccess.org/download/ Start: Ctrl+Alt+N Navigate: Arrow keys or Tab Read: NVDA+Down arrow Stop: NVDA+Q VoiceOver (Mac - Built-in): Start: Cmd+F5 Navigate: VO+Right/Left arrow (VO = Ctrl+Option) Read: VO+A (read all) Stop: Cmd+F5 What to test: Are all interactive elements announced? Are images described properly? Are form labels read with inputs? Are dynamic updates announced? Is heading structure clear?
axe DevTools (Browser extension - highly recommended): Install: Chrome/Firefox extension Run: F12 β axe DevTools tab β Scan Fix: Review violations, follow remediation Retest: Scan again after fixes Lighthouse (Built into Chrome): Open DevTools (F12) Lighthouse tab Select "Accessibility" category Generate report Score 90+ is good, 100 is ideal
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: wcag-checklist.md - Complete WCAG 2.1 Level A & AA requirements with examples semantic-html.md - Element selection guide, when to use which tag aria-patterns.md - ARIA roles, states, properties, and when to use them focus-management.md - Focus order, focus traps, focus restoration patterns color-contrast.md - Contrast requirements, testing tools, color palette tips forms-validation.md - Accessible form patterns, error handling, announcements When Claude should load these: User asks for complete WCAG checklist Deep dive into specific pattern (tabs, accordions, etc.) Color contrast issues or palette design Complex form validation scenarios
a11y-auditor.md - Automated accessibility auditor that checks pages for violations 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: Use polite for non-critical updates (notifications, counters) Use assertive for errors and critical alerts Use aria-atomic="true" to read entire region on change Keep messages concise and meaningful
React 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: <caption> - Describes table purpose scope="col" - Identifies column headers scope="row" - Identifies row headers Associates data cells with headers for screen readers
WCAG 2.1: https://www.w3.org/WAI/WCAG21/quickref/ MDN Accessibility: https://developer.mozilla.org/en-US/docs/Web/Accessibility ARIA Authoring Practices: https://www.w3.org/WAI/ARIA/apg/ WebAIM: https://webaim.org/articles/ axe DevTools: https://www.deque.com/axe/devtools/
Symptoms: 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 <div aria-live="polite"> or use role="alert"
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: All interactive elements are keyboard accessible Visible focus indicators on all focusable elements Images have alt text (or alt="" if decorative) Text contrast β₯ 4.5:1 (test with axe or Lighthouse) Form inputs have associated labels (not just placeholders) Heading hierarchy is logical (no skipped levels) Page has <html lang="en"> or appropriate language Dialogs have focus trap and restore focus on close Dynamic content uses aria-live or role="alert" Color not used alone to convey information Tested with keyboard only (no mouse) Tested with screen reader (NVDA or VoiceOver) Ran axe DevTools scan (0 violations) Lighthouse accessibility score β₯ 90 Questions? Issues? Check references/wcag-checklist.md for complete requirements Use /a11y-auditor agent to scan your page Run axe DevTools for automated testing Test with actual keyboard + screen reader Standards: WCAG 2.1 Level AA Testing Tools: axe DevTools, Lighthouse, NVDA, VoiceOver Success Criteria: 90+ Lighthouse score, 0 critical violations
Agent frameworks, memory systems, reasoning layers, and model-native orchestration.
Largest current source with strong distribution and engagement signals.