Agent Skills
Discover and share powerful Agent Skills for AI assistants
accessibility-compliance - Agent Skill - Agent Skills
Home/ Skills / accessibility-compliance Implement WCAG 2.2 compliant interfaces with mobile accessibility, inclusive design patterns, and assistive technology support. Use when auditing accessibility, implementing ARIA patterns, building for screen readers, or ensuring inclusive user experiences.
Use the skills CLI to install this skill with one command. Auto-detects all installed AI assistants.
Method 1 - skills CLI
npx skills i wshobson/agents/plugins/ui-design/skills/accessibility-compliance CopyMethod 2 - openskills (supports sync & update)
npx openskills install wshobson/agents CopyAuto-detects Claude Code, Cursor, Codex CLI, Gemini CLI, and more. One install, works everywhere.
Installation Path
Download and extract to one of the following locations:
Claude Code Cursor OpenCode Gemini CLI Codex CLI
~/.claude/skills/accessibility-compliance/ Back No setup needed. Let our cloud agents run this skill for you.
Select Model
Claude Haiku 4.5 $0.10 Claude Sonnet 4.5 $0.20 Claude Opus 4.5 $0.50 Claude Sonnet 4.5 $0.20 /task
Best for coding tasks
Try NowEnvironment setup included
Accessibility Compliance
Master accessibility implementation to create inclusive experiences that work for everyone, including users with disabilities.
When to Use This Skill
Implementing WCAG 2.2 Level AA or AAA compliance
Building screen reader accessible interfaces
Adding keyboard navigation to interactive components
Implementing focus management and focus trapping
Creating accessible forms with proper labeling
Supporting reduced motion and high contrast preferences
Building mobile accessibility features (iOS VoiceOver, Android TalkBack)
Conducting accessibility audits and fixing violations
Core Capabilities
1. WCAG 2.2 Guidelines
Perceivable: Content must be presentable in different ways
Operable: Interface must be navigable with keyboard and assistive tech
Understandable: Content and operation must be clear
Robust: Content must work with current and future assistive technologies
2. ARIA Patterns
Roles: Define element purpose (button, dialog, navigation)
States: Indicate current condition (expanded, selected, disabled)
Properties: Describe relationships and additional info (labelledby, describedby)
Live regions: Announce dynamic content changes
3. Keyboard Navigation
Focus order and tab sequence
Focus indicators and visible focus states
Keyboard shortcuts and hotkeys
Focus trapping for modals and dialogs
4. Screen Reader Support
Semantic HTML structure
Alternative text for images
Proper heading hierarchy
Skip links and landmarks
5. Mobile Accessibility
Touch target sizing (44x44dp minimum)
VoiceOver and TalkBack compatibility
Gesture alternatives
Dynamic Type support
Quick Reference
WCAG 2.2 Success Criteria Checklist
Level Criterion Description A 1.1.1 Non-text content has text alternatives A 1.3.1 Info and relationships programmatically determinable A 2.1.1 All functionality keyboard accessible A 2.4.1 Skip to main content mechanism AA 1.4.3 Contrast ratio 4.5:1 (text), 3:1 (large text) AA 1.4.11 Non-text contrast 3:1 AA
Key Patterns
interface ButtonProps extends React . ButtonHTMLAttributes < HTMLButtonElement > {
variant ?: "primary" | "secondary" ;
isLoading ?: boolean ;
}
function AccessibleButton
Pattern 2: Accessible Modal Dialog
import * as React from "react" ;
import { FocusTrap } from "@headlessui/react" ;
function AccessibleForm () {
const [ errors , setErrors ] = React.
Pattern 4: Skip Navigation Link
function SkipLink () {
return (
< a
href = "#main-content"
className = { cn (
// Hidden by default, visible on focus
"sr-only focus:not-sr-only" ,
"focus:absolute focus:top-4 focus:left-4 focus:z-50" ,
"focus:bg-background focus:px-4 focus:py-2 focus:rounded-md" ,
"focus:ring-2 focus:ring-primary" ,
Pattern 5: Live Region for Announcements
function useAnnounce () {
const [ message , setMessage ] = React. useState ( "" );
const announce = React. useCallback (
( text : string , priority
Color Contrast Requirements
// Contrast ratio utilities
function getContrastRatio ( foreground : string , background : string ) : number {
const fgLuminance = getLuminance (foreground);
const bgLuminance = getLuminance (background);
const lighter = Math.
Best Practices
Use Semantic HTML : Prefer native elements over ARIA when possible
Test with Real Users : Include people with disabilities in user testing
Keyboard First : Design interactions to work without a mouse
Don't Disable Focus Styles : Style them, don't remove them
Provide Text Alternatives : All non-text content needs descriptions
Support Zoom : Content should work at 200% zoom
Announce Changes : Use live regions for dynamic content
Respect Preferences : Honor prefers-reduced-motion and prefers-contrast
Common Issues
Missing alt text : Images without descriptions
Poor color contrast : Text hard to read against background
Keyboard traps : Focus stuck in component
Missing labels : Form inputs without associated labels
Auto-playing media : Content that plays without user initiation
Inaccessible custom controls : Recreating native functionality poorly
Missing skip links : No way to bypass repetitive content
Focus order issues : Tab order doesn't match visual order
Automated : axe DevTools, WAVE, Lighthouse
Manual : VoiceOver (macOS/iOS), NVDA/JAWS (Windows), TalkBack (Android)
Simulators : NoCoffee (vision), Silktide (various disabilities)
Resources
AA 2.5.8 Target size minimum 24x24px (NEW in 2.2)
AAA 1.4.6 Enhanced contrast 7:1
AAA 2.5.5 Target size minimum 44x44px
({
children ,
variant = "primary" ,
isLoading = false ,
disabled ,
... props
} : ButtonProps ) {
return (
< button
// Disable when loading
disabled = {disabled || isLoading}
// Announce loading state to screen readers
aria-busy = {isLoading}
// Describe the button's current state
aria-disabled = {disabled || isLoading}
className = { cn (
// Visible focus ring
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2" ,
// Minimum touch target size (44x44px)
"min-h-[44px] min-w-[44px]" ,
variant === "primary" && "bg-primary text-primary-foreground" ,
(disabled || isLoading) && "opacity-50 cursor-not-allowed" ,
)}
{ ... props}
>
{isLoading ? (
<>
< span className = "sr-only" >Loading</ span >
< Spinner aria-hidden = "true" />
</>
) : (
children
)}
</ button >
);
}
interface
DialogProps
{
isOpen : boolean ;
onClose : () => void ;
title : string ;
children : React . ReactNode ;
}
function AccessibleDialog ({ isOpen , onClose , title , children } : DialogProps ) {
const titleId = React. useId ();
const descriptionId = React. useId ();
// Close on Escape key
React. useEffect (() => {
const handleKeyDown = ( e : KeyboardEvent ) => {
if (e.key === "Escape" && isOpen) {
onClose ();
}
};
document. addEventListener ( "keydown" , handleKeyDown);
return () => document. removeEventListener ( "keydown" , handleKeyDown);
}, [isOpen, onClose]);
// Prevent body scroll when open
React. useEffect (() => {
if (isOpen) {
document.body.style.overflow = "hidden" ;
}
return () => {
document.body.style.overflow = "" ;
};
}, [isOpen]);
if ( ! isOpen) return null ;
return (
< div
role = "dialog"
aria-modal = "true"
aria-labelledby = {titleId}
aria-describedby = {descriptionId}
>
{ /* Backdrop */ }
< div
className = "fixed inset-0 bg-black/50"
aria-hidden = "true"
onClick = {onClose}
/>
{ /* Focus trap container */ }
< FocusTrap >
< div className = "fixed inset-0 flex items-center justify-center p-4" >
< div className = "bg-background rounded-lg shadow-lg max-w-md w-full p-6" >
< h2 id = {titleId} className = "text-lg font-semibold" >
{title}
</ h2 >
< div id = {descriptionId}>{children}</ div >
< button
onClick = {onClose}
className = "absolute top-4 right-4"
aria-label = "Close dialog"
>
< X className = "h-4 w-4" />
</ button >
</ div >
</ div >
</ FocusTrap >
</ div >
);
}
useState
<
Record
<
string
,
string
>>({});
return (
< form aria-describedby = "form-errors" noValidate >
{ /* Error summary for screen readers */ }
{Object. keys (errors). length > 0 && (
< div
id = "form-errors"
role = "alert"
aria-live = "assertive"
className = "bg-destructive/10 border border-destructive p-4 rounded-md mb-4"
>
< h2 className = "font-semibold text-destructive" >
Please fix the following errors:
</ h2 >
< ul className = "list-disc list-inside mt-2" >
{Object. entries (errors). map (([ field , message ]) => (
< li key = {field}>
< a href = { `#${ field }` } className = "underline" >
{message}
</ a >
</ li >
))}
</ ul >
</ div >
)}
{ /* Required field with error */ }
< div className = "space-y-2" >
< label htmlFor = "email" className = "block font-medium" >
Email address
< span aria-hidden = "true" className = "text-destructive ml-1" >
*
</ span >
< span className = "sr-only" >(required)</ span >
</ label >
< input
id = "email"
name = "email"
type = "email"
required
aria-required = "true"
aria-invalid = { !! errors.email}
aria-describedby = {errors.email ? "email-error" : "email-hint" }
className = { cn (
"w-full px-3 py-2 border rounded-md" ,
errors.email && "border-destructive" ,
)}
/>
{errors.email ? (
< p id = "email-error" className = "text-sm text-destructive" role = "alert" >
{errors.email}
</ p >
) : (
< p id = "email-hint" className = "text-sm text-muted-foreground" >
We'll never share your email.
</ p >
)}
</ div >
< button type = "submit" className = "mt-4" >
Submit
</ button >
</ form >
);
}
)}
>
Skip to main content
</ a >
);
}
// In layout
function Layout ({ children }) {
return (
<>
< SkipLink />
< header >...</ header >
< nav aria-label = "Main navigation" >...</ nav >
< main id = "main-content" tabIndex = { - 1 }>
{children}
</ main >
< footer >...</ footer >
</>
);
}
:
"polite"
|
"assertive"
=
"polite"
)
=>
{
setMessage ( "" ); // Clear first to ensure re-announcement
setTimeout (() => setMessage (text), 100 );
},
[],
);
const Announcer = () => (
< div
role = "status"
aria-live = "polite"
aria-atomic = "true"
className = "sr-only"
>
{message}
</ div >
);
return { announce, Announcer };
}
// Usage
function SearchResults ({ results , isLoading }) {
const { announce , Announcer } = useAnnounce ();
React. useEffect (() => {
if ( ! isLoading && results) {
announce ( `${ results . length } results found` );
}
}, [results, isLoading, announce]);
return (
<>
< Announcer />
< ul >{ /* results */ }</ ul >
</>
);
}
max
(fgLuminance, bgLuminance);
const darker = Math. min (fgLuminance, bgLuminance);
return (lighter + 0.05 ) / (darker + 0.05 );
}
// WCAG requirements
const CONTRAST_REQUIREMENTS = {
// Normal text (<18pt or <14pt bold)
normalText: {
AA: 4.5 ,
AAA: 7 ,
},
// Large text (>=18pt or >=14pt bold)
largeText: {
AA: 3 ,
AAA: 4.5 ,
},
// UI components and graphics
uiComponents: {
AA: 3 ,
},
};