Design a reusable toast notification system from scratch.
A context/store holding an array of toasts, an imperative API (toast.success(...)), a portal-rendered container, auto-dismiss timers, and per-toast config (type, duration, action). Discuss queueing, positioning, accessibility (aria-live), and animations.
A toast system is a small but complete component-architecture question: global state, imperative API, portals, timers, accessibility.
Architecture
1. A store + provider — toasts are global UI state. A Context (or a tiny external store like Zustand) holds toasts: Toast[].
const ToastContext = createContext(null);
function ToastProvider({ children }) {
const [toasts, setToasts] = useState([]);
const remove = useCallback((id) =>
setToasts((t) => t.filter((x) => x.id !== id)), []);
const add = useCallback((toast) => {
const id = crypto.randomUUID();
setToasts((t) => [...t, { id, duration: 4000, type: "info", ...toast }]);
return id;
}, []);
return (
<ToastContext.Provider value={{ add, remove }}>
{children}
<ToastContainer toasts={toasts} onDismiss={remove} />
</ToastContext.Provider>
);
}2. An imperative API — callers want toast.success("Saved"), not to thread context everywhere. Expose a hook useToast() returning { success, error, info, custom }, or a module-level singleton that the provider registers into.
3. Portal-rendered container — render the stack via createPortal into document.body so toasts escape parent overflow/z-index/transform contexts. Position it (top-right, bottom-center…) via a prop.
4. Auto-dismiss — each toast sets a setTimeout(duration) to remove itself; pause on hover/focus, resume on leave. Provide duration: Infinity for sticky toasts.
5. Per-toast config — type (success/error/info/warning), duration, action (button + callback), dismissible.
The things that separate a good answer
- Queueing / limiting — cap visible toasts (e.g. 3); queue the rest and promote as slots free up, so a burst doesn't bury the screen.
- Accessibility — the container is an
aria-liveregion (politefor info,assertivefor errors) so screen readers announce toasts; each toast is dismissible by keyboard. - Animations — enter/exit transitions; exit needs the toast to stay mounted until the animation finishes (
AnimatePresenceor a manual "exiting" state). - Stacking — newest on top or bottom; smooth reflow when one is removed.
- De-duplication — optional: collapse identical rapid toasts.
The framing
"It's a global store of Toast[] behind a provider, with an imperative useToast() API so callers just say toast.error(...). The container is portaled to body to escape stacking contexts. Each toast auto-dismisses on a timer that pauses on hover. The senior details are queueing to cap visible toasts, an aria-live region for screen readers, and keeping toasts mounted through their exit animation."
Follow-up questions
- •How would you queue toasts so they don't all stack at once?
- •Why render the container in a portal?
- •How do you make toasts accessible to screen readers?
- •How do you handle the exit animation without unmounting too early?
Common mistakes
- •Rendering toasts inline instead of in a portal — clipped by overflow/z-index.
- •No pause-on-hover, so a toast with an action disappears before it's clicked.
- •No aria-live region — screen reader users never hear the toast.
- •Unmounting immediately, killing the exit animation.
- •No limit on visible toasts — a burst floods the screen.
Performance considerations
- •Toast counts are small, so rendering is cheap. The real concern is timer hygiene — clear timeouts on unmount/dismiss to avoid setting state on removed toasts, and avoid re-rendering the whole app when the toast array changes (keep the store separate from app state).
Edge cases
- •A burst of 50 toasts at once — must queue/cap.
- •A sticky toast (duration: Infinity) that only dismisses on user action.
- •Toast triggered from outside React (e.g. an API layer) — needs a singleton API.
- •Very long message text or an action button changing toast height.
Real-world examples
- •react-hot-toast and Sonner — exactly this architecture: store + portal + imperative API.
- •Form-save confirmations and error banners across an app.