Skip to content

Components

React Toasts Without a Library — a Queue, a Timer and One Singleton Done Right

Why toast() Is Module State, Not Context — and the aria-live Line That Makes It Accessible
Written by Aiden Wood Founder and Lead Design Engineer
🍞 ⏲️ 📣

A toast system is three things: a queue of messages, a timer per message, and a region that announces them. react-hot-toast and sonner are both fine libraries — but if that's your only client-side dependency, you're shipping a runtime for something that fits in a file.

Velocity X ships it as standard-toast-stack: one React file, stacking, pause-on-hover, exit animations, installed as source you own.

The Money Pattern

The design decision that matters: toast() must be callable from anywhere — an event handler, a fetch catch, a Zustand action — so it can't live in React context. It's module state with a subscriber:

type Toast = { id: number; kind: 'success' | 'error'; text: string };

let queue: Toast[] = [];
let notify: (() => void) | null = null;

export function toast(kind: Toast['kind'], text: string) {
  queue = [...queue, { id: Date.now(), kind, text }];
  notify?.();
}

export function useToasts() {
  return useSyncExternalStore(
    (cb) => { notify = cb; return () => { notify = null; }; },
    () => queue,
  );
}

useSyncExternalStore is the hook everyone forgets exists — it's React's official bridge to exactly this kind of external state, no context provider wrapping your app, no re-render storms.

The Catch

Two, and they're both invisible in a demo. First: one stack per app. The moment two components each mount their own toast region, every toast renders twice — and because each copy looks correct alone, nobody notices until a user does. The component mounts once in the app shell, full stop.

Second, the accessibility line that most hand-rolled versions skip:

<div role="status" aria-live="polite" class="fixed bottom-4 right-4">

Without aria-live, a toast is a visual-only event — a screen reader user's "saved" confirmation simply never happens. polite for confirmations, assertive only for errors that block the task. And pause the dismiss timer on hover and on focus; keyboard users hover with Tab.

The Verdict

Toasts are a 150-line problem with two sharp edges: singleton discipline and the live region. Own those lines instead of renting them — browse standard-toast-stack and the rest of Standard Overlays & Menus.

Let us make some quick suggestions?

Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.