CodePark/ questions / Classic designs
START PRACTICINGPRACTICE
Frontend question · Frontend system design › Classic designs

Design a notifications / toast system for a large app. How do any component and non-React code trigger a toast?

toast.success("Saved");
toast.error("Upload failed", {
  action: { label: "Retry", onClick: retry },
  duration: Infinity,
});
const id = toast.loading("Uploading...");
toast.update(id, { type: "success", message: "Uploaded" });

Keep the toast state in a tiny store outside React, so any code can call toast(). Render the toasts with one Toaster component near the root.

Requirements:

  • success, error, info and loading toasts
  • an optional action button
  • auto-dismiss, paused on hover and focus
  • stacking, with a maximum count
  • callable from anywhere

Non-functional: accessible, no storms of re-renders, and small.

Architecture: the store is a module with subscribe, add, update and dismiss. Because it lives outside React, components, data-layer callbacks and plain utilities can all call toast(). A single Toaster component mounted near the root subscribes with useSyncExternalStore. It renders the toasts in a portal. Each toast has an id, type, message, duration and an optional action.

Behaviour:

  • Queue toasts beyond a visible limit.
  • Remove duplicate identical messages.
  • Pause timers on hover, on focus, and when the tab is hidden.
  • Error toasts with actions stay until the user dismisses them.
  • Loading toasts can be updated in place.

Accessibility: a live region is an area that screen readers read aloud when it changes. A polite one waits for a pause. An assertive one interrupts.

  • Use a polite live region for status messages, and an assertive one only for urgent errors.
  • Toasts must not steal focus.
  • Actions are reachable by keyboard.
  • Give users enough time to read.
  • Critical information never lives only in a toast.

Trade-offs: a global store is simple but implicit. App-wide inbox notifications are a different system: a separate feed backed by the server.

  • Keep the store outside React, so any code can trigger toasts.
  • A single Toaster subscribes to the store with useSyncExternalStore.
  • Queue and deduplicate toasts, and pause timers on hover and focus.
  • Use live regions, and never steal focus.
  • Never put critical information only in a toast.