Tips & Tricks (Updated: 6/2/2026)

Build Accessible React Toast Notifications with Claude Code

React toast guide with queueing, auto-dismiss, pause controls, aria-live, reduced motion, and Claude Code prompts.

Build Accessible React Toast Notifications with Claude Code

Toast notifications look small, but they sit at the intersection of product feedback, accessibility, mobile layout, and revenue. A good toast says “Saved”, “Export started”, or “Payment failed” without blocking the current task. A weak toast disappears before people can read it, interrupts screen readers with noisy alerts, covers the primary CTA on mobile, or becomes the only place where an important error appears.

Claude Code can generate a decent-looking toast component quickly. The publishable version needs a sharper prompt: queueing, auto-dismiss, pause on hover and focus, role="status" versus role="alert", reduced motion, safe-area spacing, and review checks. This guide gives you a copy-paste React implementation and the prompts I would use to ask Claude Code for the work.

For adjacent reading, pair this with Claude Code accessibility, animation implementation, responsive design, and React development.

Design Rules

A toast is not a modal. It should not trap focus or stop the user. Use it for short, non-blocking status messages. If the user must make a decision, fix a field, confirm a destructive action, or recover from a payment problem, keep the message in the page as well.

The implementation below follows these rules:

  • show at most three toasts at once
  • use role="status" for success, info, and warning messages
  • use role="alert" only for errors that need immediate attention
  • auto-dismiss, but pause while the toast is hovered or focused
  • include a close button for every toast
  • respect prefers-reduced-motion
  • avoid mobile notches and home bars with CSS safe-area values

Official references worth keeping open are MDN on the status role, MDN on the alert role, W3C WCAG Status Messages, W3C WCAG Pause, Stop, Hide, MDN setTimeout, MDN prefers-reduced-motion, and the Claude Code docs.

Copy-Paste React Code

Create ToastProvider.tsx. It uses React only. In a Next.js App Router project, add "use client"; at the top.

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

Add 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: #ffffff;
  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;
  }
}

Use it from your app.

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: "Profile saved",
        description: "Your changes will appear the next time this screen loads.",
      });
    } catch {
      showToast({
        tone: "error",
        title: "Save failed",
        description: "Check your connection and try again.",
        durationMs: 8000,
      });
    }
  }

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

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

Real Use Cases

The first use case is a settings or profile screen. A short success toast after saving reassures the user without forcing a modal. Field-level validation still belongs near the field. A toast can summarize that three fields need attention, but it should never be the only error location.

The second use case is a background task such as CSV export, AI summary generation, image processing, or email delivery. Toasts work well for “started”, “completed”, and “failed” states. Ask Claude Code to implement all three states, not just the happy path.

The third use case is a monetized content flow. A toast can confirm that a free PDF was sent, a consultation request was received, or a paid download is being prepared. It must not cover the pricing CTA, Gumroad link, newsletter form, or bottom sticky mobile button. For content funnels, connect this with Claude Code analytics implementation so you can measure whether notifications help or hurt conversion.

Pitfalls

Do not make every toast an alert. role="alert" is assertive and can interrupt assistive technology output. Use it sparingly for urgent failures, such as lost connection or a session expiring before unsaved work can be preserved.

Do not dismiss too quickly. Two seconds is often unreadable after translation or on small screens. Five seconds is a reasonable default; error messages usually need longer. Pause on hover and focus gives mouse and keyboard users time to read and close the message.

Do not rely on the toast as the only record of important information. Payment errors, permission problems, and form validation issues need persistent in-page messages. A toast disappears by design.

Do not add looping motion or blinking. Keep the entrance animation short, and disable it for reduced-motion users. WCAG guidance around moving and auto-updating content exists because uncontrolled movement can distract people and interfere with reading.

Claude Code Prompts

Use this implementation prompt:

Implement accessible React + TypeScript toast notifications.
Only edit ToastProvider.tsx and toast.css.

Requirements:
- success/info/warning/error tones
- maximum 3 visible toasts
- auto-dismiss, close button, pause on hover and focus
- role="status" for non-urgent messages
- role="alert" only for urgent errors
- aria-atomic="true"
- prefers-reduced-motion support
- mobile safe-area spacing
- no important form error should exist only inside a toast

Verification:
- npm run typecheck
- npm run lint
- manually test success, error, queue overflow, hover pause, focus pause, and keyboard close

Then review the diff with:

Review this toast implementation critically.
Check aria-live/status/alert choices, auto-dismiss timing, pause-on-hover/focus,
reduced motion, mobile safe areas, keyboard use, and whether important information disappears.
Return findings first with severity, file, line, and a concrete fix.

For team workflows, add the checklist to CLAUDE.md best practices and the review workflow checklist.

Hands-On Verification

I tested the pattern in a small React project by separating ToastProvider.tsx and toast.css, then triggering success, error, more than three consecutive toasts, hover pause, focus pause, close button activation, and reduced-motion behavior. The remaining timeout is stored in a ref, so a paused toast resumes with the remaining time instead of restarting the full duration. In production, add a Playwright interaction test, axe scan, and at least one manual screen-reader pass.

Conclusion

Toast notifications are small, but they affect accessibility, trust, mobile conversion, and support load. When you ask Claude Code for this feature, specify the behavior and review criteria, not just the visual component.

Solo developers can start with the free Claude Code cheatsheet. If you want ready-made prompts and setup material, use the ClaudeCodeLab products. Teams that need repeatable UI review, accessibility rules, and monetization-safe implementation can start with Claude Code training and consultation.

#Claude Code #toast #notification #React #UI
Free

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.