Communitygithub.com

frontend-a11y

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

Was ist frontend-a11y?

Das Skill „frontend-a11y“ bietet praktische Barrierefreiheitsmuster für React- und Next.js-Anwendungen. Es konzentriert sich auf die häufigsten Probleme, die in Code-Reviews beanstandet werden: fehlende formularbezogene Beschriftungen, falsche Verwendung von ARIA-Attributen, nicht-semantische interaktive Elemente und eine defekte Tastaturnavigation. Es ist besonders nützlich für Entwickler, die Formulare, modale Dialoge, Dropdowns, Tooltips und andere interaktive Komponenten erstellen oder überprüfen. Das Skill enthält klare Vorher-Nachher-Beispiele für jedes Muster, eine vollständige Checkliste für Code-Reviews und behandelt wichtige Konzepte wie Focus-Management, semantisches HTML, dynamische Inhalte mit aria-live und die Respektierung der Betriebssystemeinstellung „Reduzierte Bewegung“. Es unterstützt die gängigsten Frameworks und Tools wie React, Next.js, CodeRabbit und ESLint mit a11y-Regeln. Entwickler können mit diesem Skill sicherstellen, dass ihre Komponenten für Screenreader, Tastaturnutzer und alle anderen Nutzer zugänglich sind.

Funktioniert mit~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/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

frontend-a11y preview
frontend-a11y preview
frontend-a11y preview

Dokumentation

Barrierefreiheitsmuster für das Frontend

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

Wann zu aktivieren

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

Formularzugänglichkeit

Fehlende htmlFor/id-Paarung 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 entspricht der ID des Eingabefelds
<label htmlFor="email">E-Mail</label>
<input id="email" type="email" />

Pflichtfelder

// SCHLECHT: Nur optischer Stern sagt 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

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

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

// GUT: button ist fokussierbar, aktiviert mit Eingabetaste/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: Anchor unterstützt Rechtsklick, Mittelklick und Tastaturnavigation
<a href="/home">Startseite</a>
// SCHLECHT: Überschriftenhierarchie übersprungen (h1 zu h4)
<h1>Dashboard</h1>
<h4>Letzte Aktivität</h4>

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

ARIA-Attribute

Verwende ARIA nur, wenn die nativen HTML-Semantiken nicht ausreichen. Falsches ARIA ist schlimmer als kein ARIA.

aria-label vs aria-labelledby

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

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

aria-describedby

// Stellt eine ergänzende 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

// Verwende aria-live, um Inhalte anzukündigen, die sich ohne Seitenneuladung aktualisieren
// polite: wartet, bis der Benutzer die aktuelle Aktion beendet hat, bevor er etwas ankündigt
// assertive: unterbricht sofort – verwende nur für dringende Fehler

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

Fokusverwaltung

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/Shift+Tab-Zyklus innerhalb des Modals) verwende 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: Aussagekräftiges Bild mit beschreibendem Alt-Text
<img src="/chart.png" alt="Monatlicher Umsatz stieg von Januar bis März um 23 %" />

// GUT: Symbol-Schaltfläche mit zugänglicher Beschriftung
<button aria-label="Element löschen">
  <TrashIcon aria-hidden="true" />
</button>

Reduzierte Bewegung

Respektiere Benutzer, die in ihren Betriebssystemeinstellungen eine 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 Tastatur-Handler
<div role="button" onClick={handleClick}>Absenden</div>
// Fehlt: tabIndex={0}, onKeyDown für Eingabetaste/Leertaste

Checkliste

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 Fokusfalle mit Tab/Shift+Tab-Zyklus eine Bibliothek wie focus-trap-react verwenden)
  • Dynamische Inhaltsaktualisierungen verwenden aria-live
  • prefers-reduced-motion wird für Animationen respektiert

Verwandte Skills

  • 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 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

Verwandte Skills