Tips & Tricks (Atualizado: 02/06/2026)

Notificações toast acessíveis em React com Claude Code

Guia React para toast com fila, auto-dismiss, pausa, aria-live, reduced motion e safe areas móveis.

Notificações toast acessíveis em React com Claude Code

Notificações toast parecem pequenas, mas afetam acessibilidade, confiança, mobile e conversão. Um bom toast diz “Salvo”, “Exportação iniciada” ou “Pagamento falhou” sem bloquear a tarefa. Um toast ruim some rápido demais, usa alert para tudo, cobre o CTA principal ou vira o único lugar onde um erro importante aparece.

Claude Code gera a parte visual rapidamente. Para ter qualidade de produção, o prompt precisa pedir fila, auto-dismiss, pausa em hover e focus, escolha entre role="status" e role="alert", suporte a prefers-reduced-motion, safe areas móveis e revisão crítica. Leia também Claude Code accessibility, animation implementation, responsive design e React development.

Regras de design

Toast não é modal. Ele não deve roubar foco nem interromper o fluxo. Use para feedback curto e não bloqueante. Se a pessoa precisa corrigir um campo, confirmar uma ação destrutiva, recuperar pagamento ou ler instruções longas, mantenha uma mensagem persistente na página.

Esta implementação segue estas regras:

  • no máximo 3 toasts visíveis
  • success, info e warning usam role="status"
  • error usa role="alert" apenas quando for urgente
  • fechamento automático com pausa em hover e focus
  • botão de fechar em todos os toasts
  • animação removida em prefers-reduced-motion
  • espaçamento mobile com env(safe-area-inset-*)

Use fontes oficiais: MDN status role, MDN alert role, W3C WCAG Status Messages, W3C WCAG Pause, Stop, Hide, MDN setTimeout, MDN prefers-reduced-motion e Claude Code docs.

Código React copiável

Crie ToastProvider.tsx. Ele depende apenas de React. Em Next.js App Router, adicione "use client"; no topo.

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="Notificações">
      {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={`Fechar ${toast.title}`} onClick={() => onDismiss(toast.id)}>
        ×
      </button>
    </section>
  );
}

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

Exemplo de uso:

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: "Perfil salvo",
        description: "As mudanças aparecerão quando esta tela for aberta novamente.",
      });
    } catch {
      showToast({
        tone: "error",
        title: "Falha ao salvar",
        description: "Verifique a conexão e tente outra vez.",
        durationMs: 8000,
      });
    }
  }

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

export default function App() {
  return (
    <ToastProvider>
      <main>
        <h1>Configurações</h1>
        <SaveProfileButton />
      </main>
    </ToastProvider>
  );
}

Casos reais

Primeiro: tela de configurações. O toast confirma que o salvamento terminou sem abrir modal. Erros de campo continuam perto do campo; o toast só resume.

Segundo: tarefas em background, como exportar CSV, gerar resumo com IA, processar imagem ou enviar e-mail. Peça a Claude Code estados de início, sucesso, falha, cancelamento e retry.

Terceiro: funil de monetização. Um toast pode confirmar PDF gratuito enviado, solicitação de consultoria recebida ou download pago preparado. Ele não pode cobrir preço, link Gumroad, formulário ou botão fixo inferior. Para medir, conecte com Claude Code analytics implementation.

Armadilhas

Não use role="alert" para tudo. Mensagens não urgentes devem usar status; alert fica para falhas que exigem atenção imediata.

Não feche rápido demais. Cinco segundos é um padrão razoável, e erros precisam de mais tempo. Pausa em hover/focus ajuda mouse e teclado.

Não deixe informação crítica só no toast. Falha de pagamento, permissão insuficiente e validação de formulário precisam ficar na página.

Evite piscar e animações em loop. A entrada deve ser curta e desligada com reduced motion.

Prompts para Claude Code

Implemente notificações toast acessíveis em React + TypeScript.
Edite apenas ToastProvider.tsx e toast.css.

Requisitos:
- tons success/info/warning/error
- máximo de 3 toasts visíveis
- auto-dismiss, botão fechar, pausa em hover/focus
- role="status" para mensagens não urgentes
- role="alert" apenas para erros urgentes
- aria-atomic="true"
- suporte a prefers-reduced-motion
- safe-area mobile
- erro importante de formulário nunca só no toast

Verificação:
- npm run typecheck
- npm run lint
- testar sucesso, erro, mais de 3 toasts, hover, focus e fechar pelo teclado
Revise criticamente esta implementação de toast.
Verifique status/alert, tempo de auto-dismiss, pausa hover/focus,
reduced motion, safe areas, teclado e informações importantes que desaparecem.
Retorne achados por severidade com arquivo, linha e correção.

Coloque esses critérios em CLAUDE.md best practices e no review workflow checklist.

Verificação prática

Testei o padrão em um pequeno projeto React com ToastProvider.tsx e toast.css separados: sucesso, erro, mais de três toasts, pausa em hover, pausa em focus, botão fechar e reduced motion. O tempo restante fica em useRef, então a pausa não reinicia os cinco segundos. Em produção, adicione Playwright, axe e uma checagem manual com leitor de tela.

Conclusão

Toasts são pequenos, mas afetam acessibilidade, confiança e conversão mobile. Ao pedir para Claude Code, especifique comportamento, limites e revisão, não apenas o visual.

Para estudar sozinho, comece pelo cheatsheet gratuito de Claude Code. Para prompts e materiais prontos, veja os produtos ClaudeCodeLab. Para equipes, a consultoria e treinamento Claude Code ajuda a padronizar review de UI, acessibilidade e monetização.

#Claude Code #toast #notification #React #UI
Grátis

PDF grátis: cheatsheet do Claude Code

Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.

Cuidamos dos seus dados e não enviamos spam.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.