Back to React
React
medium
mid

How would you manage overlapping toasts using 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.

4 min read·~20 min to think through

Naive toast systems show every notification at once and overlap visually. A real-world one needs a queue with policy.

Data model

ts
type Priority = "error" | "warning" | "info" | "success";

type Toast = {
  id: string;
  message: string;
  priority: Priority;
  dedupeKey?: string;       // collapse duplicates
  durationMs?: number;
  createdAt: number;
};

Store

ts
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

NeedUI
Transient confirmation ("Saved")Toast
Critical error blocking workDialog or banner
Action result with undoToast 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.

Senior engineer discussion

Seniors clarify when a toast is the wrong UI (critical info → banner/dialog), design for preemption and dedupe up front, and treat a11y as a primary requirement, not a polish item.

Related questions