Tips & Tricks (Aktualisiert: 2.6.2026)

Barrierefreie React-Toast-Benachrichtigungen mit Claude Code bauen

React-Guide für Toasts mit Queue, Auto-Dismiss, Pause, aria-live, Reduced Motion und mobilen Safe Areas.

Barrierefreie React-Toast-Benachrichtigungen mit Claude Code bauen

Toast-Benachrichtigungen wirken klein: “Gespeichert”, “Export gestartet”, “Zahlung fehlgeschlagen”. In echten Produkten beeinflussen sie aber Barrierefreiheit, Vertrauen, mobile Conversion und Support-Aufwand. Ein schlechter Toast verschwindet zu früh, nutzt überall alert, verdeckt den wichtigsten CTA oder ist der einzige Ort, an dem ein wichtiger Fehler steht.

Claude Code kann die sichtbare Komponente schnell schreiben. Für eine produktionsreife Umsetzung braucht der Prompt mehr: Queue, Auto-Dismiss, Pause bei Hover und Fokus, role="status" statt role="alert" für nicht dringende Hinweise, prefers-reduced-motion, mobile Safe Areas und Review-Fragen. Ergänzend passen Claude Code Accessibility, Animation Implementation, Responsive Design und React Development.

Designregeln

Ein Toast ist kein Modal. Er darf den Fokus nicht fangen und die Aufgabe nicht blockieren. Verwende ihn für kurze, nicht blockierende Statusmeldungen. Wenn Nutzer ein Feld korrigieren, eine riskante Aktion bestätigen oder eine Zahlung retten müssen, gehört die Information zusätzlich dauerhaft in die Seite.

Diese Umsetzung folgt klaren Regeln:

  • maximal 3 sichtbare Toasts
  • success, info und warning mit role="status"
  • error nur bei dringenden Fehlern mit role="alert"
  • automatisches Schließen, pausiert bei Hover und Fokus
  • Schließen-Button pro Toast
  • keine Animation bei prefers-reduced-motion
  • mobile Abstände mit env(safe-area-inset-*)

Offizielle Quellen: MDN status role, MDN alert role, W3C WCAG Status Messages, W3C WCAG Pause, Stop, Hide, MDN setTimeout, MDN prefers-reduced-motion und Claude Code Docs.

Kopierbarer React-Code

Lege ToastProvider.tsx an. Die Datei braucht nur React. In Next.js App Router gehört "use client"; an den Anfang.

import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
  type ReactNode,
} from "react";

type ToastTone = "success" | "info" | "warning" | "error";
type ToastInput = { title: string; description?: string; tone?: ToastTone; durationMs?: number };
type ToastItem = Required<Omit<ToastInput, "durationMs">> & { id: string; durationMs: number; createdAt: number };
type ToastContextValue = { showToast: (input: ToastInput) => string; dismissToast: (id: string) => void };

const ToastContext = createContext<ToastContextValue | null>(null);
const MAX_VISIBLE_TOASTS = 3;
const DEFAULT_DURATION = 5000;

export function ToastProvider({ children }: { children: ReactNode }) {
  const [toasts, setToasts] = useState<ToastItem[]>([]);
  const dismissToast = useCallback((id: string) => {
    setToasts((current) => current.filter((toast) => toast.id !== id));
  }, []);
  const showToast = useCallback((input: ToastInput) => {
    const id = crypto.randomUUID();
    const nextToast: ToastItem = {
      id,
      title: input.title,
      description: input.description ?? "",
      tone: input.tone ?? "info",
      durationMs: input.durationMs ?? DEFAULT_DURATION,
      createdAt: Date.now(),
    };
    setToasts((current) => [...current, nextToast].slice(-MAX_VISIBLE_TOASTS));
    return id;
  }, []);
  const value = useMemo(() => ({ showToast, dismissToast }), [showToast, dismissToast]);
  return (
    <ToastContext.Provider value={value}>
      {children}
      <ToastViewport toasts={toasts} onDismiss={dismissToast} />
    </ToastContext.Provider>
  );
}

export function useToast() {
  const context = useContext(ToastContext);
  if (!context) throw new Error("useToast must be used inside ToastProvider");
  return context;
}

function ToastViewport({ toasts, onDismiss }: { toasts: ToastItem[]; onDismiss: (id: string) => void }) {
  return (
    <div className="toast-viewport" aria-label="Benachrichtigungen">
      {toasts.map((toast) => (
        <ToastCard key={toast.id} toast={toast} onDismiss={onDismiss} />
      ))}
    </div>
  );
}

function ToastCard({ toast, onDismiss }: { toast: ToastItem; onDismiss: (id: string) => void }) {
  const [paused, setPaused] = useState(false);
  const remainingMs = useRef(toast.durationMs);
  const startedAt = useRef<number | null>(null);
  const timeoutId = useRef<number | null>(null);

  useEffect(() => {
    if (toast.durationMs <= 0 || paused) return;
    startedAt.current = Date.now();
    timeoutId.current = window.setTimeout(() => onDismiss(toast.id), remainingMs.current);
    return () => {
      if (timeoutId.current !== null) window.clearTimeout(timeoutId.current);
      if (startedAt.current !== null) remainingMs.current -= Date.now() - startedAt.current;
    };
  }, [onDismiss, paused, toast.durationMs, toast.id]);

  const role = toast.tone === "error" ? "alert" : "status";
  return (
    <section
      className={`toast-card toast-card--${toast.tone}`}
      role={role}
      aria-atomic="true"
      onMouseEnter={() => setPaused(true)}
      onMouseLeave={() => setPaused(false)}
      onFocus={() => setPaused(true)}
      onBlur={() => setPaused(false)}
    >
      <div className="toast-card__content">
        <strong className="toast-card__title">{toast.title}</strong>
        {toast.description ? <p>{toast.description}</p> : null}
      </div>
      <button type="button" className="toast-card__close" aria-label={`${toast.title} schließen`} onClick={() => onDismiss(toast.id)}>
        ×
      </button>
    </section>
  );
}

Ergänze toast.css.

.toast-viewport {
  position: fixed;
  top: max(16px, env(safe-area-inset-top));
  right: max(16px, env(safe-area-inset-right));
  z-index: 1000;
  display: grid;
  gap: 10px;
  width: min(380px, calc(100vw - 32px));
  pointer-events: none;
}
.toast-card {
  pointer-events: auto;
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: start;
  gap: 12px;
  padding: 14px 14px 14px 16px;
  border: 1px solid #d8dee8;
  border-left-width: 5px;
  border-radius: 8px;
  background: #fff;
  color: #172033;
  box-shadow: 0 12px 30px rgba(15, 23, 42, 0.18);
  animation: toast-slide-in 180ms ease-out;
}
.toast-card--success { border-left-color: #15803d; }
.toast-card--info { border-left-color: #2563eb; }
.toast-card--warning { border-left-color: #b45309; }
.toast-card--error { border-left-color: #b91c1c; }
.toast-card__title { display: block; font-size: 0.95rem; line-height: 1.35; }
.toast-card p { margin: 4px 0 0; color: #46536a; font-size: 0.875rem; line-height: 1.5; }
.toast-card__close {
  min-width: 32px;
  min-height: 32px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: #526071;
  cursor: pointer;
  font-size: 1.25rem;
  line-height: 1;
}
.toast-card__close:hover,
.toast-card__close:focus-visible { background: #eef2f7; outline: 2px solid transparent; }
@keyframes toast-slide-in {
  from { opacity: 0; transform: translateY(-8px); }
  to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 640px) {
  .toast-viewport { left: 16px; right: 16px; width: auto; }
}
@media (prefers-reduced-motion: reduce) {
  .toast-card { animation: none; }
}

Verwendung:

import { ToastProvider, useToast } from "./ToastProvider";
import "./toast.css";

function SaveProfileButton() {
  const { showToast } = useToast();

  async function handleSave() {
    try {
      await new Promise((resolve) => window.setTimeout(resolve, 600));
      showToast({
        tone: "success",
        title: "Profil gespeichert",
        description: "Die Änderungen erscheinen beim nächsten Öffnen dieser Ansicht.",
      });
    } catch {
      showToast({
        tone: "error",
        title: "Speichern fehlgeschlagen",
        description: "Prüfe die Verbindung und versuche es erneut.",
        durationMs: 8000,
      });
    }
  }

  return <button onClick={handleSave}>Speichern</button>;
}

export default function App() {
  return (
    <ToastProvider>
      <main>
        <h1>Einstellungen</h1>
        <SaveProfileButton />
      </main>
    </ToastProvider>
  );
}

Praxisfälle

Erstens: Einstellungen speichern. Ein Erfolgstoast bestätigt die Aktion ohne Modal. Feldfehler bleiben am Feld; der Toast darf nur zusammenfassen.

Zweitens: Hintergrundjobs wie CSV-Export, KI-Zusammenfassung, Bildverarbeitung oder E-Mail-Versand. Plane Start, Erfolg, Fehler, Abbruch und Retry.

Drittens: Monetarisierung. Ein Toast kann bestätigen, dass ein kostenloses PDF verschickt, eine Beratung angefragt oder ein Download vorbereitet wurde. Er darf Preis, Gumroad-Link, Newsletter-Formular oder mobilen Sticky-Button nicht verdecken. Messe die Wirkung mit Claude Code Analytics Implementation.

Häufige Fehler

Nutze nicht überall role="alert". Nicht dringende Meldungen gehören in status; alert ist für sofort relevante Fehler.

Schließe nicht zu schnell. Fünf Sekunden sind ein guter Standard, Fehler brauchen länger. Hover- und Fokus-Pause gibt Maus- und Tastaturnutzern Zeit.

Lege kritische Informationen nie nur in Toasts ab. Zahlungsfehler, Berechtigungen und Formularvalidierung müssen dauerhaft sichtbar bleiben.

Vermeide Blinken und Endlosanimationen. Die Eintrittsanimation ist kurz und wird bei Reduced Motion abgeschaltet.

Claude-Code-Prompts

Implementiere barrierefreie Toast-Benachrichtigungen mit React + TypeScript.
Bearbeite nur ToastProvider.tsx und toast.css.

Anforderungen:
- success/info/warning/error
- maximal 3 sichtbare Toasts
- Auto-Dismiss, Schließen-Button, Pause bei hover/focus
- role="status" für nicht dringende Meldungen
- role="alert" nur für dringende Fehler
- aria-atomic="true"
- prefers-reduced-motion
- mobile safe-area
- wichtige Formularfehler nie nur im Toast

Verifikation:
- npm run typecheck
- npm run lint
- Erfolg, Fehler, mehr als 3 Toasts, Hover-Pause, Fokus-Pause und Tastatur-Schließen testen
Reviewe diese Toast-Implementierung kritisch.
Prüfe status/alert, Auto-Dismiss-Zeit, hover/focus pause,
reduced motion, mobile safe areas, Tastaturbedienung und verschwindende wichtige Informationen.
Gib Findings nach Schwere mit Datei, Zeile und konkreter Korrektur aus.

Diese Punkte passen in CLAUDE.md Best Practices und die Review Workflow Checklist.

Verifikation

Ich habe das Muster in einem kleinen React-Projekt mit getrennten Dateien getestet: Erfolg, Fehler, mehr als drei Toasts, Hover-Pause, Fokus-Pause, Schließen-Button und Reduced Motion. Die Restzeit liegt in useRef, daher startet der Timer nach einer Pause nicht wieder bei fünf Sekunden. Für Produktion ergänze Playwright, axe und einen manuellen Screenreader-Test.

Fazit

Toasts sind klein, aber sie beeinflussen Barrierefreiheit, Vertrauen und mobile Conversion. Bitte Claude Code nicht nur um eine visuelle Komponente, sondern um Verhalten, Grenzen und Review-Kriterien.

Allein startest du mit dem kostenlosen Claude Code Cheatsheet. Für fertige Prompts und Setup-Materialien nutze die ClaudeCodeLab Produkte. Teams können UI-Review, Accessibility und Monetarisierung über Claude Code Training und Beratung standardisieren.

#Claude Code #toast #notification #React #UI
Kostenlos

Kostenloses PDF: Claude-Code-Cheatsheet

E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.

Wir schützen Ihre Daten und senden keinen Spam.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.