Tips & Tricks (Mis à jour: 02/06/2026)

Créer des notifications toast React accessibles avec Claude Code

Guide React pour des toast avec file, fermeture auto, pause, aria-live, reduced motion et safe areas mobiles.

Créer des notifications toast React accessibles avec Claude Code

Une notification toast semble secondaire, mais elle touche l’accessibilité, le mobile, la confiance et la conversion. Un bon toast annonce “Enregistré”, “Export lancé” ou “Paiement échoué” sans bloquer la tâche. Un mauvais toast disparaît trop vite, interrompt un lecteur d’écran avec trop de alert, recouvre le CTA principal ou devient le seul endroit où une erreur importante est visible.

Claude Code peut générer l’apparence très vite. Pour un composant publiable, il faut demander la file d’attente, la fermeture automatique, la pause au survol et au focus, le choix entre role="status" et role="alert", prefers-reduced-motion, les safe areas mobiles et une revue critique. À lire aussi : accessibilité avec Claude Code, animations, responsive design et développement React.

Règles de conception

Un toast n’est pas une modale. Il ne doit pas voler le focus ni bloquer l’utilisateur. Utilisez-le pour un retour court et non bloquant. Si l’utilisateur doit corriger un champ, confirmer une action risquée ou récupérer un paiement, gardez aussi un message persistant dans la page.

Cette version suit ces règles :

  • au maximum 3 toast visibles
  • success, info et warning en role="status"
  • error en role="alert" seulement pour les urgences
  • fermeture automatique avec pause au survol et au focus
  • bouton de fermeture sur chaque toast
  • animation désactivée avec prefers-reduced-motion
  • espacement mobile avec env(safe-area-inset-*)

Sources officielles : MDN status role, MDN alert role, W3C WCAG Status Messages, W3C WCAG Pause, Stop, Hide, MDN setTimeout, MDN prefers-reduced-motion et Claude Code docs.

Code React copiable

Créez ToastProvider.tsx. Il ne dépend que de React. Avec Next.js App Router, ajoutez "use client"; en haut.

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="Notifications">
      {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={`Fermer ${toast.title}`} onClick={() => onDismiss(toast.id)}>
        ×
      </button>
    </section>
  );
}

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

Exemple d’utilisation :

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 enregistré",
        description: "Les changements apparaîtront lors de la prochaine ouverture.",
      });
    } catch {
      showToast({
        tone: "error",
        title: "Échec de l’enregistrement",
        description: "Vérifiez votre connexion puis réessayez.",
        durationMs: 8000,
      });
    }
  }

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

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

Cas d’usage

Premier cas : une page de paramètres. Un toast de succès confirme l’enregistrement sans ouvrir de modale. Les erreurs de champs restent près des champs.

Deuxième cas : une tâche en arrière-plan comme export CSV, résumé IA, traitement d’image ou envoi d’e-mail. Demandez à Claude Code les états démarré, réussi, échoué, annulé et relancé.

Troisième cas : un parcours monétisé. Le toast peut confirmer l’envoi d’un PDF gratuit, une demande de conseil ou la préparation d’un téléchargement payant. Il ne doit jamais recouvrir le prix, le lien Gumroad, le formulaire newsletter ou le bouton mobile fixe. Mesurez l’impact avec Claude Code analytics implementation.

Pièges fréquents

N’utilisez pas role="alert" partout. Les messages non urgents doivent rester en status; alert est réservé aux erreurs qui demandent une attention immédiate.

Ne fermez pas trop vite. Cinq secondes est un bon défaut, et les erreurs méritent plus. La pause au survol et au focus donne du temps aux utilisateurs souris et clavier.

Ne placez pas une information critique uniquement dans un toast. Paiement échoué, permission insuffisante et validation de formulaire doivent rester dans la page.

Évitez les animations en boucle et le clignotement. L’animation d’entrée doit être courte et coupée en reduced motion.

Prompts Claude Code

Implémente des notifications toast accessibles en React + TypeScript.
Ne modifie que ToastProvider.tsx et toast.css.

Exigences:
- tons success/info/warning/error
- maximum 3 toast visibles
- fermeture automatique, bouton fermer, pause hover/focus
- role="status" pour les messages non urgents
- role="alert" seulement pour les erreurs urgentes
- aria-atomic="true"
- support prefers-reduced-motion
- safe-area mobile
- aucune erreur importante uniquement dans un toast

Vérification:
- npm run typecheck
- npm run lint
- tester succès, erreur, plus de 3 toast, pause hover/focus et fermeture clavier
Relis cette implémentation de toast de façon critique.
Vérifie status/alert, durée d’autoclosing, pause hover/focus,
reduced motion, safe areas, clavier et informations qui disparaissent.
Retourne les problèmes par sévérité avec fichier, ligne et correctif.

Ajoutez ces règles à CLAUDE.md best practices et à la review workflow checklist.

Vérification terrain

J’ai testé ce modèle dans un petit projet React avec ToastProvider.tsx et toast.css séparés : succès, erreur, plus de trois toast, pause au survol, pause au focus, bouton fermer et reduced motion. Le temps restant est stocké dans useRef, donc la reprise ne redémarre pas à cinq secondes. En production, ajoutez Playwright, axe et une vérification manuelle avec lecteur d’écran.

Conclusion

Un toast est petit, mais il influence accessibilité, confiance et conversion mobile. Avec Claude Code, demandez le comportement, les limites et la revue, pas seulement le composant visuel.

Pour progresser seul, commencez par le cheatsheet Claude Code gratuit. Pour des prompts et supports prêts à l’emploi, consultez les produits ClaudeCodeLab. Pour standardiser revue UI, accessibilité et monétisation en équipe, utilisez la formation et consultation Claude Code.

#Claude Code #toast #notification #React #UI
Gratuit

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.