Communitygithub.com

junksidetm/darkside-toolkit

Barrierefreiheit-Muster für React und Next.js – semantisches HTML, ARIA-Attribute, Formularbeschriftung, Tastaturnavigation, Fokus-Management und Screenreader-Unterstützung. Verwenden Sie es beim Erstellen oder Überprüfen von Formularen, Modalen, Dropdowns, Tooltips oder Tabs, beim Beheben von a11y-Lint- oder Code-Review-Fundstellen oder beim Einrichten von Tastaturnavigation und Fokus-Management.

Was ist darkside-toolkit?

Diese Fertigkeit bietet einen umfassenden Leitfaden zur Implementierung von Barrierefreiheitsmustern in React- und Next.js-Anwendungen. Sie deckt die häufigsten Probleme ab, die in Code-Reviews gefunden werden, wie fehlende Formularbeschriftungen, falsche ARIA-Nutzung, nicht-semantische interaktive Elemente und defekte Tastaturnavigation. Die Fertigkeit enthält konkrete, kopierbare Codebeispiele für Formularzugänglichkeit (Label-Verknüpfung, Pflichtfelder, Fehlermeldungen), semantisches HTML, ARIA-Attribute (aria-label, aria-labelledby, aria-describedby, aria-live, aria-expanded), Tastaturnavigation (z. B. benutzerdefinierte Dropdowns mit Tastenunterstützung), Fokus-Management (Modalfokus-Wiederherstellung) sowie den Umgang mit Bildern, Ikonen und reduzierter Bewegung. Sie zeigt Anti-Patterns auf und bietet eine Checkliste zur Qualitätssicherung. Die Muster sind plattformunabhängig für React und Next.js und helfen Entwicklern, die Anforderungen der WCAG (Web Content Accessibility Guidelines) zu erfüllen.

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

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Vorschau

Aus dem Skill-README

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

Dokumentation

Frontend-Barrierefreiheitsmuster

Praktische Barrierefreiheitsmuster für React und Next.js. Behandelt die Probleme, die am häufigsten in Code-Reviews beanstandet werden: fehlende Formularbeschriftungen, falsche ARIA-Nutzung, nicht-semantische interaktive Elemente und defekte Tastaturnavigation.

Wann aktivieren

  • Beim Erstellen oder Überprüfen von Formularkomponenten (<input>, <select>, <textarea>)
  • Beim Erstellen interaktiver Elemente (Modals, Dropdowns, Tooltips, Tabs)
  • Bei Verwendung von <div> oder <span> mit onClick
  • Beim Hinzufügen von aria-*-Attributen zu einem Element
  • Bei der Implementierung von Tastaturnavigation oder Fokus-Management
  • Beim Erhalt von Barrierefreiheits-Feedback von Code-Review-Tools (CodeRabbit, ESLint a11y)
  • Beim Erstellen von Komponenten, die Screenreader unterstützen müssen

Formularzugänglichkeit

Fehlende htmlFor/id-Verknüpfungen und getrennte Fehlermeldungen sind die häufigsten Probleme, die in Code-Reviews beanstandet werden.

Beschriftungsverbindung

// SCHLECHT: Label hat keine Verbindung zum Eingabefeld – Screenreader können sie nicht zuordnen
<label>E-Mail</label>
<input type="email" />

// GUT: htmlFor stimmt mit der id des Eingabefelds überein
<label htmlFor="email">E-Mail</label>
<input id="email" type="email" />

Pflichtfelder

// SCHLECHT: Nur visuelles Sternchen vermittelt Screenreadern nichts
<label htmlFor="email">E-Mail *</label>
<input id="email" type="email" />

// GUT: required aktiviert native Browser-Validierung; aria-required signalisiert es Screenreadern
<label htmlFor="email">
  E-Mail <span aria-hidden="true">*</span>
</label>
<input id="email" type="email" required aria-required="true" />

Fehlermeldungen

// SCHLECHT: Fehlertext ist visuell vorhanden, aber nicht mit dem Eingabefeld verknüpft
<input id="email" type="email" />
<span className="error">Ungültige E-Mail-Adresse</span>

// GUT: aria-describedby verbindet das Eingabefeld mit seiner Fehlermeldung
// aria-invalid signalisiert den ungültigen Zustand an Screenreader
<input
  id="email"
  type="email"
  aria-describedby="email-error"
  aria-invalid={!!error}
/>
{error && (
  <span id="email-error" role="alert">
    {error}
  </span>
)}

Vollständiges barrierefreies Formular

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 = 'E-Mail ist erforderlich';
    if (!password) newErrors.password = 'Passwort ist erforderlich';
    if (Object.keys(newErrors).length) {
      setErrors(newErrors);
      return;
    }
    onSubmit(email, password);
  };

  return (
    <form onSubmit={handleSubmit} noValidate>
      <div>
        <label htmlFor="email">
          E-Mail <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">
          Passwort <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">Anmelden</button>
    </form>
  );
}

Semantisches HTML

Verwenden Sie das Element, das der Absicht entspricht. Screenreader und Tastaturnutzer sind auf native Semantik angewiesen.

// SCHLECHT: div hat keine Rolle, keine Tastaturunterstützung, keinen barrierefreien Namen
<div onClick={handleClick}>Absenden</div>

// GUT: button ist fokussierbar, aktiviert bei Enter/Leertaste, kündigt sich als "Schaltfläche" an
<button type="button" onClick={handleClick}>Absenden</button>
// SCHLECHT: nicht-semantische Navigation
<div onClick={() => navigate('/home')}>Startseite</div>

// GUT: Anker unterstützt Rechtsklick, Mittelklick und Tastaturnavigation
<a href="/home">Startseite</a>
// SCHLECHT: Überschriftenhierarchie übersprungen (h1 nach h4)
<h1>Dashboard</h1>
<h4>Letzte Aktivität</h4>

// GUT: aufeinanderfolgende Überschriftenebenen
<h1>Dashboard</h1>
<h2>Letzte Aktivität</h2>

ARIA-Attribute

Verwenden Sie ARIA nur, wenn native HTML-Semantik nicht ausreicht. Falsches ARIA ist schlimmer als kein ARIA.

aria-label vs. aria-labelledby

// aria-label: Inline-String-Beschriftung – verwenden, wenn kein sichtbarer Beschriftungstext vorhanden ist
<button aria-label="Modal schließen">
  <XIcon />
</button>

// aria-labelledby: Verweist auf den Text eines anderen Elements – verwenden, wenn eine sichtbare Beschriftung vorhanden ist
<section aria-labelledby="section-title">
  <h2 id="section-title">Letzte Bestellungen</h2>
  {/* Inhalt */}
</section>

aria-describedby

// Stellt eine zusätzliche Beschreibung über die Beschriftung hinaus bereit
<button
  aria-describedby="delete-warning"
  onClick={handleDelete}
> Konto löschen
</button>
<p id="delete-warning">Diese Aktion kann nicht rückgängig gemacht werden.</p>

aria-live für dynamische Inhalte

// Verwenden Sie aria-live, um Inhalte anzukündigen, die ohne Seitenneuladung aktualisiert werden
// polite: wartet, bis der Benutzer die aktuelle Aktion abgeschlossen hat, bevor angekündigt wird
// assertive: unterbricht sofort – nur für dringende Fehler verwenden

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

Tastaturnavigation

Jedes interaktive Element muss allein mit der Tastatur erreichbar und bedienbar sein.

Benutzerdefiniertes Dropdown

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

Fokus-Management

Der Fokus muss sich logisch bewegen, wenn sich der UI-Zustand ändert – insbesondere bei Modals und Routenübergängen.

Modal-Fokus-Wiederherstellung

Dieses Beispiel behandelt den anfänglichen Fokus und die Wiederherstellung. Für eine vollständige Fokusfalle (Tab/Umschalt+Tab-Zyklus innerhalb des Modals) verwenden Sie eine Bibliothek wie focus-trap-react, die Randfälle wie dynamische Inhalte und verschachtelte Portale behandelt.

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) {
      // Aktuell fokussiertes Element speichern und Fokus in das Modal verschieben
      previousFocusRef.current = document.activeElement as HTMLElement;
      modalRef.current?.focus();
    } else {
      // Fokus auf das Element zurücksetzen, das das Modal geöffnet hat
      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}>Schließen</button>
    </div>
  );
}

Bilder und Symbole

// SCHLECHT: Dekoratives Symbol als unbeschriftetes Bild angekündigt
<img src="/icon.svg" />

// GUT: Dekoratives Bild vor Screenreadern versteckt
<img src="/decoration.png" alt="" aria-hidden="true" />

// GUT: Bedeutungsvolles Bild mit beschreibendem Alternativtext
<img src="/chart.png" alt="Monatlicher Umsatz stieg von Januar bis März um 23 %" />

// GUT: Symbolschaltfläche mit barrierefreier Beschriftung
<button aria-label="Element löschen">
  <TrashIcon aria-hidden="true" />
</button>

Reduzierte Bewegung

Respektieren Sie Benutzer, die in ihren Betriebssystemeinstellungen reduzierte Bewegung angefordert haben.

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

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

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

Anti-Patterns

// SCHLECHT: onClick auf nicht-interaktivem Element ohne Tastaturunterstützung
<div onClick={handleClick}>Klick mich</div>

// SCHLECHT: aria-label auf einem div ohne Rolle
<div aria-label="Navigation">...</div>

// SCHLECHT: Platzhalter als Ersatz für Beschriftung verwendet
<input placeholder="Geben Sie Ihre E-Mail ein" />

// SCHLECHT: positiver tabIndex erzeugt unvorhersehbare Tab-Reihenfolge
<button tabIndex={3}>Absenden</button>

// SCHLECHT: aria-hidden auf einem fokussierbaren Element – Tastaturnutzer werden gefangen
<button aria-hidden="true">Öffnen</button>

// SCHLECHT: role="button" auf div ohne Tastaturhandler
<div role="button" onClick={handleClick}>Absenden</div>
// Fehlt: tabIndex={0}, onKeyDown für Enter/Leertaste

Checkliste

Überprüfen Sie vor dem Einreichen einer interaktiven Komponente zur Überprüfung:

  • Jedes <input>, <select> und <textarea> hat ein verbundenes <label> über htmlFor/id
  • Fehlermeldungen sind mit aria-describedby verknüpft und mit role="alert" markiert
  • Kein onClick auf <div> oder <span> ohne role, tabIndex und onKeyDown
  • Nur-Symbol-Schaltflächen haben aria-label
  • Dekorative Bilder verwenden alt="" und aria-hidden="true"
  • Modals stellen den Fokus beim Schließen wieder her (für vollständige Fokuseinfang mit Tab/Umschalt+Tab-Zyklus verwenden Sie eine Bibliothek wie focus-trap-react)
  • Dynamische Inhaltsaktualisierungen verwenden aria-live
  • prefers-reduced-motion wird für Animationen beachtet

Verwandte Fertigkeiten

  • frontend-patterns – allgemeine React-Komponenten- und Zustandsmuster
  • design-system – Design-Token- und Komponentenkonsistenz
  • motion-foundations und motion-patterns: Animationsmuster mit Barrierefreiheitsaspekten

Individual skills in this repo

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

Verwandte Skills