Toast notifications appear in nearly every web app. They look trivial; production-quality toasts are not. The interview tests whether you understand queue management, stacking, animations, and the accessibility considerations of dynamic announcements.
Functional requirements
- Show a toast on user action (success, error, warning, info). Each type gets its own color and icon so users read severity at a glance, but the type should also drive semantics — an error is not just red, it announces differently to assistive tech. Interviewers check that you connect the visual variant to the accessibility behavior rather than treating them separately.
- Auto-dismiss after a delay. Store the duration per toast, not as one global constant, so an error can outlive a quick success message. Start the timer when the toast actually becomes visible, not when it was triggered, or a toast queued behind others loses time it never spent on screen.
- Manual dismiss via close button. Every toast needs an explicit close affordance because auto-dismiss alone fails anyone who reads slowly or looks away. Wire the button to the same removal path the timer uses so both routes run the exit animation instead of yanking the node out.
- Stack multiple toasts. Decide up front whether a new toast pushes older ones aside or the newest simply replaces the oldest. The classic bug here is unbounded growth — a burst of events fills the whole viewport with toasts.
- Animate in and out. Both directions matter; a toast that vanishes instantly reads as a rendering glitch. Keep the exit shorter than the entrance so the slot frees up quickly for whatever comes next.
- Screen-reader accessible. This is the part candidates skip and interviewers weight heavily. A toast that only exists visually is completely invisible to screen-reader users, so the announcement mechanism is as much a requirement as the pixels.
Architecture
Three components:
- Toast queue: shared state for active toasts. It lives outside the component tree so any code — even a fetch interceptor or a plain utility — can push a toast. Beyond the message list, it tracks each toast’s status (entering, active, exiting) so the viewport knows what to animate.
- Toast viewport: renders the active toasts in a fixed position. Mount exactly one viewport near the root of the app; multiple viewports produce duplicate stacks when different components each try to render toasts. It subscribes to the queue and maps state to DOM.
- Trigger function: imperative API (toast.success(“Saved”)). Keep this as a plain importable function rather than a hook so you can fire a toast from an event handler, a router guard, or a catch block — anywhere, not only inside a component. All it does is dispatch into the shared store.
State management
Common pattern: a global state for toasts. Library options:
- Zustand store. A small external store fits well because toast state has to live outside React’s render tree to be callable from anywhere. It gives you a plain
toast.success()function with no provider to wire up. - React Context. Simple and dependency-free, but the trigger becomes a hook (
useToast()), so you can only fire toasts from inside components. Split the context or memoize carefully, or every consumer re-renders on each toast change. - Sonner (popular toast library) — uses its own store. You drop in a single
<Toaster />and calltoast()from anywhere; it is the answer to name when an interviewer asks what you would actually ship in production.
Each toast: { id, type, message, action?, duration }
Toast lifecycle
- Trigger function adds to queue. Assign the unique id at this step and return it to the caller, so the same call site can later update or dismiss that specific toast (this is what makes promise-based and dedup patterns work).
- Viewport renders the new toast with enter animation. Mount the node first, then trigger the transition on the next animation frame — set the initial off-screen state, then flip to the visible state — or the browser paints the final position immediately and skips the animation.
- Timer counts down toward auto-dismiss. Track a start timestamp and the remaining duration rather than a naive interval, because you will need to pause and resume it accurately.
- Pause timer when user hovers (keep toast visible). A user hovering is a signal they are reading; dismissing it out from under them is the frustration you are trying to avoid.
- Resume timer on mouse leave. Resume from the elapsed time, not a fresh full duration, so a toast the user briefly hovered still clears on roughly its original schedule.
- On dismiss: exit animation, then remove from queue. Keep the node mounted while the exit animation plays, then remove it after the transition finishes — listen for
transitionendor use a timeout that matches the animation duration.
Stacking
When multiple toasts are visible:
- Newest at top (or bottom). Pick one and stay consistent; the newest toast should sit closest to where the user’s attention already is (usually nearest the triggering action or the screen edge).
- Earlier toasts shift to make room. Animate the shift with a transform so existing toasts glide instead of jumping, which keeps the group readable during a burst.
- Cap at 3–5 visible (newer pushes oldest out). Enforce the cap in the store, not just visually — hide or drop overflow so the DOM does not grow without bound. When you evict the oldest, run its exit animation so it leaves gracefully rather than disappearing.
Modern UX: stacked-card visual with slight fan effect (Sonner-style).
Position
Common positions:
- Top right (desktop default). The most conventional spot; out of the way of primary content but still in the natural reading path on wide screens.
- Bottom right (alternate desktop). Common when the top-right corner already holds account menus or notifications; keeps toasts clear of the header.
- Bottom center (mobile-friendly). Sits near the thumb zone and clear of the notch, which is why most mobile-first apps default here.
- Top center (announcements that need attention). Reserve this for higher-priority messages, since it lands directly in the user’s line of sight and is harder to ignore.
Make configurable. Don’t cover important content.
Animations
Slide + fade in/out is the standard:
- Enter: slide from off-screen edge (200ms). Slide in from the same edge the toast is anchored to, so the motion reads as the toast arriving from outside the viewport.
- Exit: fade out (150ms). Keep the exit shorter and simpler than the entrance so a dismissed toast clears fast and does not delay the next one.
- Reposition when other toasts dismiss. Use a FLIP-style transition or a CSS transform transition so the remaining toasts slide smoothly into their new slots instead of snapping.
Animate transform and opacity (compositor-friendly).
Accessibility
- Use
role="status"for non-urgent toasts (announces politely). This queues the message so it is read after the screen reader finishes its current utterance — right for a “Saved” confirmation that is not time-critical. - Use
role="alert"for errors (interrupts screen reader). It preempts whatever is being announced, which is appropriate for failures the user must know about immediately. - aria-live region appropriate to type. The live region must already be in the DOM before the toast text lands in it; injecting the region and the text at the same moment often means nothing gets announced.
- Close button is keyboard-accessible. It has to be a real focusable button reachable by Tab and triggerable with Enter or Space, not a clickable
div. - Focus stays where it was; don’t hijack focus to the toast. Moving focus into a toast rips keyboard and screen-reader users out of their task; toasts are announced, not focused. This is a frequent point interviewers probe.
Pause on hover
Useful behavior: timer pauses while user hovers. Implementation:
- Track elapsed time. Record when the timer started and how long it should run, so you can compute the remaining time at any moment rather than relying on a single fixed timeout.
- onMouseEnter pauses timer. Clear the pending timeout and store how much time was left when the pointer entered.
- onMouseLeave resumes from elapsed time. Restart the timeout with only the remaining duration, not the full duration, so a brief hover does not reset the whole countdown. Consider pausing on keyboard focus too, so keyboard users get the same reprieve.
Action buttons
Toasts can have actions (“Undo,” “Retry”):
- Button inside the toast. Give the action a clear label and enough tap area; “Undo” is the canonical example, letting you defer the real work (like a delete) until the toast’s window closes.
- Click triggers callback. Run the caller-supplied handler, and account for it being async — a “Retry” may need its own loading state before the toast resolves.
- Toast dismisses after action. Once the action fires, remove the toast so it does not linger, and cancel its auto-dismiss timer so the two paths do not race to remove the same node.
Promise-based toasts
Modern pattern (Sonner):
toast.promise(saveAsync(), {
loading: "Saving...",
success: "Saved",
error: "Failed to save"
});
Single call updates the toast through loading → success/error.
Common antipatterns
- Toasts that dismiss too fast (errors should stay until dismissed). A 3-second error that a user glances away from is effectively no error at all; match duration to how much the user needs to read and act.
- Toasts that block important UI. A toast covering a submit button or form field turns a helpful message into an obstacle; keep the viewport clear of interactive content.
- Animations that ignore prefers-reduced-motion. Users who set this preference can get motion sickness from sliding elements; gate the slide behind a media query and fall back to a plain fade or an instant appearance.
- Spam: 10 toasts at once. A loop or a burst of failed requests can flood the screen; throttle or rate-limit the trigger, or collapse repeats into a single count (“3 items saved”).
- Confirmation toasts that feel like “we did your action” without need. Do not toast every trivial success; if the result is already visible in the UI, the toast is noise. Reserve them for outcomes the user cannot otherwise see.
Library options
- Sonner: the modern standard. Beautiful, accessible, ergonomic API.
- react-hot-toast: popular, minimal, easy to customize.
- react-toastify: older, more features.
Frequently Asked Questions
How long should a toast last?
Success: 3–4 seconds. Errors: 5–8 seconds (or persistent). Manual dismiss always available.
Should errors auto-dismiss?
Generally no. Users may miss the message. Either keep visible until dismissed or use longer duration.
How do I prevent duplicate toasts?
Pass an optional ID. If a toast with that ID exists, update it instead of adding new.
Useful next steps:
