Design a toast system any component can trigger. Apply RADIO; cover stacking, auto-dismiss, and a11y.
Global cross-cutting UI, timer management, and accessible announcements via live regions.
Requirements: transient messages (success/error/info) that stack, auto-dismiss, don't block the UI, are pausable, and are announced to screen readers. Architecture: a single ToastProvider owns a queue in context and exposes add(); a fixed-position container renders the list; a useToast() hook lets any component enqueue without prop-drilling. Data model: an array of { id, type, message, duration }; each toast owns a timer. Interface: toast.success('Saved'), toast.error(msg) — a tiny imperative API over the queue. Optimizations & a11y: render the container in an aria-live region (polite for info, assertive for errors) so messages are announced; pause auto-dismiss on hover/focus and resume on leave (someone reading a message shouldn't lose it); cap how many show and coalesce or drop the oldest to avoid a wall of toasts; give each an accessible close button; animate enter/exit but respect prefers-reduced-motion. Why a queue in one provider: toasts are global UI, so colocating them avoids z-index/stacking wars and lets any component fire one. The timer detail people miss: clear the timeout on unmount and on manual dismiss, and when pausing, store the REMAINING time so resume doesn't restart the full duration. Keep the provider render-cheap (split state from the dispatch API / memoize the context value) so a new toast doesn't re-render the whole app.
Toasts, snackbars, inline alerts — any transient global feedback multiple components must trigger.
const ToastCtx = React.createContext(null);
function ToastProvider({ children }) {
const [toasts, setToasts] = React.useState([]);
const remove = React.useCallback(
(id) => setToasts((t) => t.filter((x) => x.id !== id)), []);
const add = React.useCallback((message, type = 'info', duration = 4000) => {
const id = crypto.randomUUID();
setToasts((t) => [...t, { id, message, type }]);
setTimeout(() => remove(id), duration); // auto-dismiss
}, [remove]);
return (
<ToastCtx.Provider value={add}>
{children}
<div className="toasts" role="region" aria-live="polite">
{toasts.map((t) => (
<div key={t.id} role="status">
{t.message}
<button onClick={() => remove(t.id)} aria-label="Dismiss">×</button>
</div>
))}
</div>
</ToastCtx.Provider>
);
}
const useToast = () => React.useContext(ToastCtx);