Claude Agent Skill · by Wshobson

Accessibility Compliance

This walks you through implementing proper WCAG 2.2 compliance with real code patterns for screen readers, keyboard navigation, and mobile accessibility. It cov

Install
Terminal · npx
$npx skills add https://github.com/wshobson/agents --skill accessibility-compliance
Works with Paperclip

How Accessibility Compliance fits into a Paperclip company.

Accessibility Compliance drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md412 lines
Expand
---name: accessibility-compliancedescription: 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.--- # 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    | 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                          | ## Key Patterns ### Pattern 1: Accessible Button ```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>  );}``` ### Pattern 2: Accessible Modal Dialog ```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>  );}``` ### Pattern 3: Accessible Form ```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>  );}``` ### Pattern 4: Skip Navigation Link ```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 layoutfunction Layout({ children }) {  return (    <>      <SkipLink />      <header>...</header>      <nav aria-label="Main navigation">...</nav>      <main id="main-content" tabIndex={-1}>        {children}      </main>      <footer>...</footer>    </>  );}``` ### Pattern 5: Live Region for Announcements ```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 };} // Usagefunction 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>    </>  );}``` ## Color Contrast Requirements ```typescript// Contrast ratio utilitiesfunction 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 requirementsconst 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,  },};``` ## Best Practices 1. **Use Semantic HTML**: Prefer native elements over ARIA when possible2. **Test with Real Users**: Include people with disabilities in user testing3. **Keyboard First**: Design interactions to work without a mouse4. **Don't Disable Focus Styles**: Style them, don't remove them5. **Provide Text Alternatives**: All non-text content needs descriptions6. **Support Zoom**: Content should work at 200% zoom7. **Announce Changes**: Use live regions for dynamic content8. **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 ## Testing Tools - **Automated**: axe DevTools, WAVE, Lighthouse- **Manual**: VoiceOver (macOS/iOS), NVDA/JAWS (Windows), TalkBack (Android)- **Simulators**: NoCoffee (vision), Silktide (various disabilities)