Communitygithub.com

junksidetm/darkside-toolkit

Modèles d'accessibilité pour React et Next.js — HTML sémantique, attributs ARIA, étiquetage de formulaires, navigation clavier, gestion du focus et support des lecteurs d'écran. À utiliser lors de la création ou de la révision de formulaires, modales, listes déroulantes, infobulles ou onglets, de la correction de violations d'accessibilité ESLint ou de revue de code, ou de la mise en place de navigation clavier et de gestion du focus.

Qu'est-ce que darkside-toolkit ?

Cette compétence fournit des modèles d'accessibilité concrets pour les applications React et Next.js, ciblant les problèmes les plus fréquemment soulevés lors des revues de code. Elle couvre en détail l'étiquetage correct des formulaires, l'utilisation appropriée des attributs ARIA, le HTML sémantique, la navigation au clavier et la gestion du focus. La documentation comprend des exemples de code directement applicables, des anti-modèles à éviter et une checklist pratique pour valider les composants interactifs avant soumission. Elle est conçue pour être utilisée avec des outils de revue de code comme CodeRabbit ou ESLint a11y. La compétence met l'accent sur l'importance d'utiliser les éléments HTML natifs avant d'ajouter des attributs ARIA et fournit des modèles complets pour les formulaires, les modales, les listes déroulantes et les animations respectueuses des préférences de mouvement réduit.

Compatible avec~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/junksidetm/darkside-toolkit/tree/HEAD/skills/frontend-a11y

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Aperçu

Depuis le README du skill

darkside-toolkit preview
darkside-toolkit preview
darkside-toolkit preview

Documentation

Modèles d'accessibilité frontend

Modèles d'accessibilité pratiques pour React et Next.js. Couvre les problèmes les plus fréquemment soulevés lors des revues de code : étiquettes de formulaire manquantes, utilisation incorrecte d'ARIA, éléments interactifs non sémantiques et navigation clavier défaillante.

Quand activer cette compétence

  • Création ou révision de composants de formulaire (<input>, <select>, <textarea>)
  • Création d'éléments interactifs (modales, listes déroulantes, infobulles, onglets)
  • Utilisation de <div> ou <span> avec onClick
  • Ajout d'attributs aria-* à un élément
  • Mise en œuvre de la navigation clavier ou de la gestion du focus
  • Réception de retours d'accessibilité d'outils de revue de code (CodeRabbit, ESLint a11y)
  • Création de composants devant prendre en charge les lecteurs d'écran

Accessibilité des formulaires

L'absence d'appariement htmlFor / id et les messages d'erreur déconnectés sont les problèmes les plus courants soulevés lors des revues de code.

Connexion des étiquettes

// MAUVAIS : l'étiquette n'a aucun lien avec le champ — les lecteurs d'écran ne peuvent pas les associer
<label>Email</label>
<input type="email" />

// BON : htmlFor correspond à l'id du champ
<label htmlFor="email">Email</label>
<input id="email" type="email" />

Champs obligatoires

// MAUVAIS : l'astérisque visuel seulement n'est pas transmis aux lecteurs d'écran
<label htmlFor="email">Email *</label>
<input id="email" type="email" />

// BON : required active la validation native du navigateur ; aria-required le signale aux lecteurs d'écran
<label htmlFor="email">
  Email <span aria-hidden="true">*</span>
</label>
<input id="email" type="email" required aria-required="true" />

Messages d'erreur

// MAUVAIS : le texte d'erreur existe visuellement mais n'est pas lié au champ
<input id="email" type="email" />
<span className="error">Adresse e-mail invalide</span>

// BON : aria-describedby relie le champ à son message d'erreur
// aria-invalid signale l'état invalide aux lecteurs d'écran
<input
  id="email"
  type="email"
  aria-describedby="email-error"
  aria-invalid={!!error}
/>
{error && (
  <span id="email-error" role="alert">
    {error}
  </span>
)}

Formulaire accessible complet

interface LoginFormProps {
  onSubmit: (email: string, password: string) => void;
}

export function LoginForm({ onSubmit }: LoginFormProps) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState<{ email?: string; password?: string }>({});

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    const newErrors: typeof errors = {};
    if (!email) newErrors.email = 'Email requis';
    if (!password) newErrors.password = 'Mot de passe requis';
    if (Object.keys(newErrors).length) {
      setErrors(newErrors);
      return;
    }
    onSubmit(email, password);
  };

  return (
    <form onSubmit={handleSubmit} noValidate>
      <div>
        <label htmlFor="email">
          Email <span aria-hidden="true">*</span>
        </label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={e => setEmail(e.target.value)}
          aria-required="true"
          aria-describedby={errors.email ? 'email-error' : undefined}
          aria-invalid={!!errors.email}
          autoComplete="email"
        />
        {errors.email && (
          <span id="email-error" role="alert">
            {errors.email}
          </span>
        )}
      </div>

      <div>
        <label htmlFor="password">
          Mot de passe <span aria-hidden="true">*</span>
        </label>
        <input
          id="password"
          type="password"
          value={password}
          onChange={e => setPassword(e.target.value)}
          aria-required="true"
          aria-describedby={errors.password ? 'password-error' : undefined}
          aria-invalid={!!errors.password}
          autoComplete="current-password"
        />
        {errors.password && (
          <span id="password-error" role="alert">
            {errors.password}
          </span>
        )}
      </div>

      <button type="submit">Se connecter</button>
    </form>
  );
}

HTML sémantique

Utilisez l'élément qui correspond à l'intention. Les lecteurs d'écran et les utilisateurs de clavier dépendent de la sémantique native.

// MAUVAIS : div n'a pas de rôle, pas de support clavier, pas de nom accessible
<div onClick={handleClick}>Soumettre</div>

// BON : button est focusable, s'active avec Entrée/Espace, annoncé comme "bouton"
<button type="button" onClick={handleClick}>Soumettre</button>
// MAUVAIS : navigation non sémantique
<div onClick={() => navigate('/home')}>Accueil</div>

// BON : l'ancre prend en charge le clic droit, le clic central et la navigation clavier
<a href="/home">Accueil</a>
// MAUVAIS : hiérarchie de titres sautée (h1 à h4)
<h1>Tableau de bord</h1>
<h4>Activité récente</h4>

// BON : niveaux de titres séquentiels
<h1>Tableau de bord</h1>
<h2>Activité récente</h2>

Attributs ARIA

N'utilisez ARIA que lorsque la sémantique HTML native ne suffit pas. Un mauvais ARIA est pire que pas d'ARIA.

aria-label vs aria-labelledby

// aria-label : étiquette textuelle en ligne — à utiliser lorsqu'aucun texte d'étiquette visible n'existe
<button aria-label="Fermer la modale">
  <XIcon />
</button>

// aria-labelledby : référence le texte d'un autre élément — à utiliser lorsqu'une étiquette visible existe
<section aria-labelledby="section-title">
  <h2 id="section-title">Commandes récentes</h2>
  {/* contenu */}
</section>

aria-describedby

// Fournit une description supplémentaire au-delà de l'étiquette
<button
  aria-describedby="delete-warning"
  onClick={handleDelete}
> Supprimer le compte
</button>
<p id="delete-warning">Cette action est irréversible.</p>

aria-live pour le contenu dynamique

// Utilisez aria-live pour annoncer le contenu qui se met à jour sans rechargement de page
// polite : attend que l'utilisateur termine l'action en cours avant d'annoncer
// assertive : interrompt immédiatement — à utiliser uniquement pour les erreurs urgentes

export function StatusMessage({ message, isError }: { message: string; isError?: boolean }) {
  return (
    <div role="status" aria-live={isError ? 'assertive' : 'polite'} aria-atomic="true">
      {message}
    </div>
  );
}

aria-expanded et aria-controls

export function Accordion({ title, children }: { title: string; children: React.ReactNode }) {
  const [isOpen, setIsOpen] = useState(false);
  const contentId = useId();

  return (
    <div>
      <button aria-expanded={isOpen} aria-controls={contentId} onClick={() => setIsOpen(prev => !prev)}>
        {title}
      </button>
      <div id={contentId} hidden={!isOpen}>
        {children}
      </div>
    </div>
  );
}

Navigation clavier

Chaque élément interactif doit être atteignable et utilisable uniquement avec le clavier.

Liste déroulante personnalisée

export function Dropdown({ options, onSelect }: { options: string[]; onSelect: (value: string) => void }) {
  const [isOpen, setIsOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(0);
  const listId = useId();

  if (!options.length) return null;

  const handleKeyDown = (e: React.KeyboardEvent) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        setActiveIndex(i => Math.min(i + 1, options.length - 1));
        break;
      case 'ArrowUp':
        e.preventDefault();
        setActiveIndex(i => Math.max(i - 1, 0));
        break;
      case 'Enter':
      case ' ':
        e.preventDefault();
        if (isOpen) onSelect(options[activeIndex]);
        setIsOpen(prev => !prev);
        break;
      case 'Escape':
        setIsOpen(false);
        break;
    }
  };

  return (
    <div
      role="combobox"
      aria-expanded={isOpen}
      aria-haspopup="listbox"
      aria-controls={listId}
      tabIndex={0}
      onKeyDown={handleKeyDown}
      onClick={() => setIsOpen(prev => !prev)}
    >
      <span>{options[activeIndex]}</span>
      {isOpen && (
        <ul id={listId} role="listbox">
          {options.map((option, index) => (
            <li
              key={option}
              role="option"
              aria-selected={index === activeIndex}
              onClick={() => {
                onSelect(option);
                setIsOpen(false);
              }}
            >
              {option}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

Gestion du focus

Le focus doit se déplacer logiquement lorsque l'état de l'interface utilisateur change — en particulier pour les modales et les transitions de route.

Restauration du focus d'une modale

Cet exemple couvre le focus initial et la restauration. Pour un piège à focus complet (cycle Tab/Maj+Tab à l'intérieur de la modale), utilisez une bibliothèque comme focus-trap-react qui gère les cas limites comme le contenu dynamique et les portails imbriqués.

export function Modal({ isOpen, onClose, title, children }: { isOpen: boolean; onClose: () => void; title: string; children: React.ReactNode }) {
  const modalRef = useRef<HTMLDivElement>(null);
  const previousFocusRef = useRef<HTMLElement | null>(null);

  useEffect(() => {
    if (isOpen) {
      // Enregistre l'élément actuellement focus et déplace le focus dans la modale
      previousFocusRef.current = document.activeElement as HTMLElement;
      modalRef.current?.focus();
    } else {
      // Restaure le focus sur l'élément qui a ouvert la modale
      previousFocusRef.current?.focus();
    }
  }, [isOpen]);

  if (!isOpen) return null;

  return (
    <div ref={modalRef} role="dialog" aria-modal="true" aria-labelledby="modal-title" tabIndex={-1} onKeyDown={e => e.key === 'Escape' && onClose()}>
      <h2 id="modal-title">{title}</h2>
      {children}
      <button onClick={onClose}>Fermer</button>
    </div>
  );
}

Images et icônes

// MAUVAIS : icône décorative annoncée comme image non étiquetée
<img src="/icon.svg" />

// BON : image décorative masquée pour les lecteurs d'écran
<img src="/decoration.png" alt="" aria-hidden="true" />

// BON : image pertinente avec texte alternatif descriptif
<img src="/chart.png" alt="Le chiffre d'affaires mensuel a augmenté de 23 % de janvier à mars" />

// BON : bouton icône avec étiquette accessible
<button aria-label="Supprimer l'élément">
  <TrashIcon aria-hidden="true" />
</button>

Mouvement réduit

Respectez les utilisateurs qui ont demandé un mouvement réduit dans les paramètres de leur système d'exploitation.

export function useReducedMotion(): boolean {
  const [prefersReduced, setPrefersReduced] = useState(false);

  useEffect(() => {
    const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
    setPrefersReduced(mq.matches);
    const handler = (e: MediaQueryListEvent) => setPrefersReduced(e.matches);
    mq.addEventListener('change', handler);
    return () => mq.removeEventListener('change', handler);
  }, []);

  return prefersReduced;
}

// Utilisation
export function AnimatedCard({ children }: { children: React.ReactNode }) {
  const reduceMotion = useReducedMotion();

  return (
    <div
      style={{
        transition: reduceMotion ? 'none' : 'transform 300ms ease'
      }}
    >
      {children}
    </div>
  );
}

Anti-modèles

// MAUVAIS : onClick sur un élément non interactif sans support clavier
<div onClick={handleClick}>Cliquez ici</div>

// MAUVAIS : aria-label sur une div qui n'a pas de rôle
<div aria-label="Navigation">...</div>

// MAUVAIS : placeholder utilisé comme substitut d'étiquette
<input placeholder="Entrez votre email" />

// MAUVAIS : tabIndex positif crée un ordre de tabulation imprévisible
<button tabIndex={3}>Soumettre</button>

// MAUVAIS : aria-hidden sur un élément focusable — les utilisateurs de clavier sont piégés
<button aria-hidden="true">Ouvrir</button>

// MAUVAIS : role="button" sur une div sans gestionnaire clavier
<div role="button" onClick={handleClick}>Soumettre</div>
// Manquant : tabIndex={0}, onKeyDown pour Entrée/Espace

Liste de vérification

Avant de soumettre un composant interactif à une revue :

  • Chaque <input>, <select> et <textarea> a une <label> connectée via htmlFor/id
  • Les messages d'erreur sont liés avec aria-describedby et marqués avec role="alert"
  • Pas de onClick sur <div> ou <span> sans role, tabIndex et onKeyDown
  • Les boutons avec icône uniquement ont un aria-label
  • Les images décoratives utilisent alt="" et aria-hidden="true"
  • Les modales restaurent le focus à la fermeture (pour un piège à focus complet avec cycle Tab/Maj+Tab, utilisez une bibliothèque comme focus-trap-react)
  • Les mises à jour de contenu dynamique utilisent aria-live
  • prefers-reduced-motion est respecté pour les animations

Compétences associées

  • frontend-patterns — modèles généraux de composants et d'état React
  • design-system — cohérence des tokens de design et des composants
  • motion-foundations et motion-patterns : modèles d'animation avec considérations d'accessibilité

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

Skills associés