accessibility-compliance

Par wshobson · agents

Implémenter des interfaces conformes WCAG 2.2 avec accessibilité mobile, des patterns de conception inclusive et la prise en charge des technologies d'assistance. À utiliser lors d'audits d'accessibilité, de l'implémentation de patterns ARIA, de la conception pour les lecteurs d'écran, ou pour garantir des expériences utilisateur inclusives.

npx skills add https://github.com/wshobson/agents --skill accessibility-compliance

Conformité d'accessibilité

Maîtrise la mise en œuvre de l'accessibilité pour créer des expériences inclusives qui fonctionnent pour tout le monde, y compris les utilisateurs en situation de handicap.

Quand utiliser cette compétence

  • Implémenter la conformité WCAG 2.2 niveau AA ou AAA
  • Construire des interfaces accessibles aux lecteurs d'écran
  • Ajouter la navigation au clavier aux composants interactifs
  • Implémenter la gestion du focus et le focus trapping
  • Créer des formulaires accessibles avec étiquetage approprié
  • Supporter les préférences de mouvement réduit et de contraste élevé
  • Construire des fonctionnalités d'accessibilité mobile (VoiceOver iOS, TalkBack Android)
  • Mener des audits d'accessibilité et corriger les violations

Capacités principales

1. Directives WCAG 2.2

  • Perceptible : Le contenu doit être présentable de différentes façons
  • Utilisable : L'interface doit être navigable au clavier et avec les technologies d'assistance
  • Compréhensible : Le contenu et le fonctionnement doivent être clairs
  • Robuste : Le contenu doit fonctionner avec les technologies d'assistance actuelles et futures

2. Motifs ARIA

  • Rôles : Définissent le but de l'élément (bouton, dialogue, navigation)
  • États : Indiquent la condition actuelle (développé, sélectionné, désactivé)
  • Propriétés : Décrivent les relations et infos supplémentaires (labelledby, describedby)
  • Régions dynamiques : Annoncent les changements de contenu dynamique

3. Navigation au clavier

  • Ordre du focus et séquence de tabulation
  • Indicateurs de focus et états de focus visibles
  • Raccourcis clavier et touches d'accès rapide
  • Focus trapping pour les modales et dialogues

4. Support des lecteurs d'écran

  • Structure HTML sémantique
  • Texte alternatif pour les images
  • Hiérarchie des titres appropriée
  • Liens de saut et repères

5. Accessibilité mobile

  • Dimensionnement des cibles tactiles (44x44dp minimum)
  • Compatibilité VoiceOver et TalkBack
  • Alternatives aux gestes
  • Support Dynamic Type

Référence rapide

Liste de contrôle des critères de succès WCAG 2.2

Niveau Critère Description
A 1.1.1 Le contenu non textuel a des alternatives textuelles
A 1.3.1 Info et relations déterminables par programmation
A 2.1.1 Toute fonctionnalité accessible au clavier
A 2.4.1 Mécanisme pour accéder au contenu principal
AA 1.4.3 Ratio de contraste 4,5:1 (texte), 3:1 (texte large)
AA 1.4.11 Contraste non textuel 3:1
AA 2.4.7 Focus visible
AA 2.5.8 Taille minimale de cible 24x24px (NOUVEAU dans 2.2)
AAA 1.4.6 Contraste amélioré 7:1
AAA 2.5.5 Taille minimale de cible 44x44px

Motifs clés

Motif 1 : Bouton accessible

interface 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>
  );
}

Motif 2 : Dialogue modal accessible

import * 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>
  );
}

Motif 3 : Formulaire accessible

function 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>
  );
}

Motif 4 : Lien de navigation de saut

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",
      )}
    >
      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>
    </>
  );
}

Motif 5 : Région dynamique pour les annonces

function 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>
    </>
  );
}

Exigences de contraste des couleurs

// 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,
  },
};

Bonnes pratiques

  1. Utilise le HTML sémantique : Préfère les éléments natifs à ARIA quand possible
  2. Teste avec des utilisateurs réels : Inclus les personnes en situation de handicap dans les tests d'utilisabilité
  3. Clavier en premier : Conçois les interactions pour fonctionner sans souris
  4. Ne désactive pas les styles de focus : Personnalise-les, ne les supprime pas
  5. Fournis des alternatives textuelles : Tout contenu non textuel a besoin de descriptions
  6. Supporte le zoom : Le contenu doit fonctionner à 200 % de zoom
  7. Annonce les changements : Utilise les régions dynamiques pour le contenu dynamique
  8. Respecte les préférences : Honore prefers-reduced-motion et prefers-contrast

Problèmes courants

  • Texte alt manquant : Images sans descriptions
  • Contraste de couleur faible : Texte difficile à lire sur le fond
  • Pièges au clavier : Focus coincé dans un composant
  • Étiquettes manquantes : Champs de formulaire sans étiquettes associées
  • Médias lus automatiquement : Contenu qui se lit sans initiation de l'utilisateur
  • Contrôles personnalisés inaccessibles : Recréation incorrecte de fonctionnalités natives
  • Liens de saut manquants : Pas de moyen de contourner le contenu répétitif
  • Problèmes d'ordre du focus : L'ordre de tabulation ne correspond pas à l'ordre visuel

Outils de test

  • Automatisés : axe DevTools, WAVE, Lighthouse
  • Manuels : VoiceOver (macOS/iOS), NVDA/JAWS (Windows), TalkBack (Android)
  • Simulateurs : NoCoffee (vision), Silktide (handicaps variés)

Skills similaires