How would you manage overlapping toasts or priority-based queuing
Central queue with a max visible cap, priorities (`error > warning > info > success`), and a dedupe key to merge repeat messages. Important toasts (errors) preempt; low-priority toasts wait. Each toast tracks its own timer; pause on hover; aria-live for screen readers. Use the Sonner / Radix Toast primitives instead of rolling your own.
Naive toast systems show every notification at once and overlap visually. A real-world one needs a queue with policy.
Data model
type Priority = "error" | "warning" | "info" | "success";
type Toast = {
id: string;
message: string;
priority: Priority;
dedupeKey?: string; // collapse duplicates
durationMs?: number;
createdAt: number;
};Store
const MAX_VISIBLE = 3;
const PRIO = { error: 3, warning: 2, info: 1, success: 0 };
function enqueue(t: Toast) {
// dedupe: if a toast with the same key is already visible, refresh it
const existing = visible.find((v) => v.dedupeKey && v.dedupeKey === t.dedupeKey);
if (existing) { existing.createdAt = t.createdAt; return; }
pending.push(t);
pending.sort((a, b) => PRIO[b.priority] - PRIO[a.priority] || a.createdAt - b.createdAt);
promote();
}
function promote() {
while (visible.length < MAX_VISIBLE && pending.length) {
visible.push(pending.shift()!);
}
// Preemption: if a higher-priority toast is pending and visible has a lower one, swap
if (pending[0] && visible.some((v) => PRIO[v.priority] < PRIO[pending[0].priority])) {
const lowestIdx = visible.findIndex((v) => PRIO[v.priority] < PRIO[pending[0].priority]);
pending.push(visible.splice(lowestIdx, 1)[0]);
visible.push(pending.shift()!);
}
}Timers + pause-on-hover
Each toast tracks its own timer. On hover/focus, clear the timeout; on leave, restart with the remaining time. Otherwise users can't read longer messages.
Position & stacking
- Pick a corner (top-right is conventional for desktop; bottom-center for mobile).
- Newer toasts on top; older animate out.
- Stack with subtle z-axis or shrink older ones — see Sonner's animation.
Accessibility
- Container with
role="region" aria-label="Notifications" aria-live="polite"for status / success / info. - Use
aria-live="assertive"for errors (they should interrupt). - Don't trap focus in toasts — they're announcements, not dialogs.
- Toasts with actions need to be focusable; consider not auto-dismissing those.
When to use a toast vs something else
| Need | UI |
|---|---|
| Transient confirmation ("Saved") | Toast |
| Critical error blocking work | Dialog or banner |
| Action result with undo | Toast with action button (longer duration) |
| Persistent status (offline) | Banner |
Don't use toasts for critical info — they disappear.
Anti-patterns
- Spamming 10 toasts during a failure cascade (need dedupe).
- Auto-dismissing toasts that have actions.
- Same priority handling for everything (errors get hidden behind successes).
- No screen reader announcement.
Interview framing
"Single queue, FIFO within priority, with a max visible cap. Errors preempt successes. Dedupe by key so a retry loop doesn't spam. Each toast has its own timer, paused on hover and focus. aria-live=polite by default; assertive for errors. For critical info I'd use a dialog or banner, not a toast — they're transient by design. I'd build on Radix Toast or Sonner rather than from scratch — focus management and animations are easy to get subtly wrong."
Follow-up questions
- •Why pause on hover?
- •How would you handle a toast with an undo action?
- •Differences between aria-live=polite and assertive?
Common mistakes
- •No dedupe — error storms.
- •Same dismiss timer for short and long messages.
- •Auto-dismissing toasts with actions.
- •Missing aria-live.
Performance considerations
- •Cheap. Watch for memory in the pending queue if cap isn't enforced. Animate with transforms, not layout properties.
Edge cases
- •Hundreds of toasts queued — cap pending or coalesce.
- •Toast with action focused when timer fires — don't dismiss.
- •Mobile keyboard pushes toast off screen.
Real-world examples
- •Sonner (Vercel), Radix Toast, Linear's toasts, Slack notification stack.