Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
.claude/skills/dicklesworthstone-accessibility-compliance/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-09 | ✗→✓ | ▲ Improved | 149% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 208% | 0% |
| case-01 | ✓→✗ | ▼ Worse | 105% | 0% |
| case-19 | ✓→✓ | = Same ✓ | 132% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 151% | 0% |
Master accessibility implementation to create inclusive experiences that work for everyone, including users with disabilities.
| 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 | 2.4.7 | Focus visible | | 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 |
tsxinterface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { variant?: "primary" | "secondary"; isLoading?: boolean; } function AccessibleButton({ 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> ); }
tsximport * as React from "react"; import { FocusTrap } from "@headlessui/react"; 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> ); }
tsxfunction AccessibleForm() { const [errors, setErrors] = React.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> ); }
tsxfunction 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", )} > 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> </> ); }
tsxfunction useAnnounce() { const [message, setMessage] = React.useState(""); const announce = React.useCallback( (text: string, priority: "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> </> ); }
typescript// Contrast ratio utilities function getContrastRatio(foreground: string, background: string): number { const fgLuminance = getLuminance(foreground); const bgLuminance = getLuminance(background); const lighter = Math.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, }, };
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-19 | pass→pass | 14,408 | 14,291 | -1% | 1 | 1 | 0% | 2,450 | 5,681 | +132% | 0 | 0 | — |
case-06 | fail→fail | 23,238 | 23,982 | +3% | 1 | 1 | 0% | 4,014 | 8,197 | +104% | 0 | 0 | — |
case-01 | pass→fail | 18,483 | 30,331 | +64% | 1 | 1 | 0% | 3,172 | 6,487 | +105% | 0 | 0 | — |
case-02 | pass→pass | 10,620 | 12,676 | +19% | 1 | 1 | 0% | 2,011 | 5,046 | +151% | 0 | 0 | — |
case-03 | pass→pass | 1,974 | 2,723 | +38% | 1 | 1 | 0% | 310 | 3,654 | +1079% | 0 | 0 | — |
case-04 | pass→pass | 20,008 | 17,357 | -13% | 1 | 1 | 0% | 2,841 | 5,502 | +94% | 0 | 0 | — |
case-05 | pass→pass | 25,976 | 22,173 | -15% | 1 | 1 | 0% | 5,120 | 7,464 | +46% | 0 | 0 | — |
case-07 | pass→pass | 14,254 | 15,373 | +8% | 1 | 1 | 0% | 2,683 | 6,079 | +127% | 0 | 0 | — |
case-08 | pass→pass | 13,984 | 18,898 | +35% | 1 | 1 | 0% | 2,753 | 7,018 | +155% | 0 | 0 | — |
case-09 | fail→pass | 15,337 | 14,218 | -7% | 1 | 1 | 0% | 2,307 | 5,739 | +149% | 0 | 0 | — |
case-10 | pass→pass | 10,774 | 10,747 | -0% | 1 | 1 | 0% | 2,162 | 5,345 | +147% | 0 | 0 | — |
case-11 | pass→pass | 21,040 | 14,799 | -30% | 1 | 1 | 0% | 2,958 | 5,789 | +96% | 0 | 0 | — |
case-12 | pass→pass | 23,198 | 19,816 | -15% | 1 | 1 | 0% | 3,653 | 7,326 | +101% | 0 | 0 | — |
case-13 | pass→pass | 9,358 | 9,197 | -2% | 1 | 1 | 0% | 1,802 | 4,596 | +155% | 0 | 0 | — |
case-14 | pass→pass | 10,304 | 7,067 | -31% | 1 | 1 | 0% | 1,685 | 4,361 | +159% | 0 | 0 | — |
case-15 | fail→pass | 10,085 | 14,053 | +39% | 1 | 1 | 0% | 1,753 | 5,408 | +208% | 0 | 0 | — |
case-16 | pass→pass | 12,593 | 10,303 | -18% | 1 | 1 | 0% | 2,086 | 5,050 | +142% | 0 | 0 | — |
case-17 | pass→pass | 13,967 | 17,899 | +28% | 1 | 1 | 0% | 2,251 | 6,398 | +184% | 0 | 0 | — |
case-18 | pass→pass | 14,125 | 13,697 | -3% | 1 | 1 | 0% | 2,652 | 5,665 | +114% | 0 | 0 | — |
case-20 | pass→pass | 16,191 | 23,109 | +43% | 1 | 1 | 0% | 2,796 | 7,333 | +162% | 0 | 0 | — |
case-21 | pass→pass | 12,787 | 14,216 | +11% | 1 | 1 | 0% | 2,437 | 5,409 | +122% | 0 | 0 | — |
case-22 | pass→pass | 15,903 | 19,574 | +23% | 1 | 1 | 0% | 2,550 | 6,404 | +151% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +5 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.