Toast Notifications Are a Memory Test: Designing Feedback That Respects Attention
Toasts fade in three seconds and expect users to catch, read, and act on them. Here's how we redesigned notification patterns to stop punishing distracted users and keyboard-only users alike.

Toasts are the most abused component in modern web UI. They fire for everything — a save, a warning, a five-step error the user needs to fix before Tuesday — and then vanish before anyone can read them. We've rebuilt notification systems on three products in the last year, and every time the fix starts with the same admission: the toast wasn't the problem, the decision to use a toast was.
The Toast Contract Nobody Wrote Down
A toast is a non-modal, time-limited, non-blocking message. That definition implies a contract: the information inside must be safe to miss. If a user closes their laptop mid-toast, or tabs to Slack, or is running a screen reader that hasn't reached the polite queue yet, nothing catastrophic happens.
Most product teams break this contract weekly. We see toasts used for:
- Payment failures with retry instructions
- Multi-line validation errors
- Undo actions on destructive operations
- Session-expiration warnings
- Background job completions the user has been waiting three minutes for
Each of those needs persistence, not fade-out. The rule we've landed on:
If missing this message costs the user time, money, or data, it isn't a toast.
Everything downstream — timing, position, accessibility — flows from getting that classification right.
Duration Math: Stop Guessing at Three Seconds
The default toast duration in most libraries is somewhere between 3000 and 5000 milliseconds. That's a guess dressed up as a convention. A better model, adapted from readability research and the old Nielsen guidance, is to base duration on content length.
A formula we've used with good results:
// Rough reading-based duration for toasts
// Assumes ~200 words per minute for glanceable UI copy
function toastDuration(message: string): number {
const words = message.trim().split(/\s+/).length;
const readingMs = (words / 200) * 60_000;
const minimum = 4_000; // never faster than 4s
const maximum = 10_000; // never longer than 10s; promote instead
return Math.min(Math.max(readingMs + 1_500, minimum), maximum);
}
The + 1_500 buffer accounts for the time it takes a user to notice the toast appeared at all — the classic "wait, what just flashed?" problem. If the calculated duration exceeds 10 seconds, that's your signal the message is too important or too long for a toast. Promote it to a banner or an inline block.
Pause on Hover, Pause on Focus
Any toast that auto-dismisses must pause when the user hovers it, focuses it, or when the tab loses visibility. The last one gets forgotten constantly:
document.addEventListener('visibilitychange', () => {
if (document.hidden) toastQueue.pauseAll();
else toastQueue.resumeAll();
});
Without this, a user who alt-tabs to check an email comes back to an empty screen and no idea whether their action succeeded.
Accessibility: The aria-live Trap
Here's the failure mode we see in about eight out of ten audits: a toast component uses role="alert" on every notification, regardless of severity. That makes screen readers interrupt whatever they're currently announcing — the user's own typing, a form field label, another notification — to shout the toast.
The correct mapping:
role="status"oraria-live="polite"for success and neutral inforole="alert"oraria-live="assertive"for errors and warnings that need immediate attention
And only one live region per politeness level, mounted at app start. If you create a new aria-live container per toast, most screen readers won't announce anything at all, because the region didn't exist before its content was added.
// App root — mount once
<div className="sr-only" aria-live="polite" aria-atomic="false" id="toast-live-polite" />
<div className="sr-only" aria-live="assertive" aria-atomic="false" id="toast-live-assertive" />
When a toast fires, append its text content to the matching region. Remove it after the announcement window (~1 second is enough for most engines).
Keyboard Reachability
A toast with an action button — "Undo", "Retry", "View details" — needs to be keyboard-reachable without stealing focus. The pattern:
- Toast appears; focus stays where the user left it.
- A designated shortcut (we use
F6orAlt+T) moves focus into the newest toast. Escapedismisses the focused toast and returns focus to the previous element.
Stealing focus automatically is worse than the problem you're solving. It breaks form flow and screen reader context.
Position and Motion
Bottom-center and bottom-right are the most common toast positions, and both are defensible. Top positions compete with browser chrome and system notifications. Whichever you pick, keep it consistent across the product — users learn where to look, and that learning is worth more than any A/B test result.
For motion, our default ratio is:
- Entry: 220ms, ease-out, slide + fade from 12px offset
- Exit: 160ms, ease-in, fade only
Exit should always be shorter than entry. It signals "handled" rather than "arriving", and it clears the queue faster when multiple toasts stack. Respect prefers-reduced-motion by dropping the translate and keeping only the opacity change.
@media (prefers-reduced-motion: reduce) {
.toast-enter { transform: none; transition: opacity 160ms; }
}
Queueing Without Overwhelming
The worst toast experience is five stacked notifications, each dismissing on its own timer, with the user unable to read any of them. Rules that have held up in production:
- Cap the visible queue at three. Additional toasts wait in a buffer.
- Collapse duplicates. If the same toast type fires twice in 800ms, show a single toast with a "×2" counter.
- Reset the timer on stack. When a new toast appears, existing toasts get their remaining duration extended so users aren't racing to read the oldest one.
- Group by severity. Errors never get pushed off screen by success toasts. Success can be dropped; errors cannot.
In code, that looks roughly like:
function enqueue(toast: Toast) {
const duplicate = queue.find(t =>
t.type === toast.type && t.message === toast.message && Date.now() - t.createdAt < 800
);
if (duplicate) {
duplicate.count += 1;
duplicate.createdAt = Date.now();
return;
}
if (queue.length >= 3 && toast.severity !== 'error') {
buffer.push(toast);
return;
}
queue.push(toast);
extendVisibleTimers(1_500);
}
When to Promote a Toast to Something Else
This is the decision most teams skip. A quick decision tree we hand to product designers:
- Reversible action, low stakes → toast with undo (5–8s)
- Success confirmation, no action needed → toast, short duration
- Error the user must fix in this view → inline message next to the input, not a toast
- System-wide status (maintenance, outage) → persistent banner at top of app
- Background job finished, user may have navigated away → notification center entry + optional toast
- Session about to expire → modal with countdown, not a toast
The notification center is the piece most products ship too late. Once you have one, toasts become disposable by design — the user can always find what they missed. Without it, every toast carries the weight of "this is the only time you'll see this," and the pressure to make it persistent grows until your toasts stop being toasts.
Testing Feedback Systems
Toasts are famously undertested because they're transient. A few checks worth automating:
- Screenshot test at 0ms, 200ms, and 500ms post-mount to catch entry animation regressions.
- Assert
aria-liveregion contents after firing each toast type. - Simulate
document.hiddenand verify timers pause. - Fire 10 toasts in rapid succession and assert the queue caps at three visible.
Manual QA should include one pass with a screen reader (VoiceOver or NVDA), one pass with keyboard only, and one pass with prefers-reduced-motion enabled. If your toast system passes all three, it's probably better than 90% of what's shipping.
Where We'd Start
If you're inheriting a product with toast fatigue, don't redesign the component first. Audit every toast() call in the codebase and classify each one against the decision tree above. In our experience, somewhere between a third and half of them shouldn't be toasts at all — they're inline errors, banners, or notification-center items in disguise. Fix the classification, then fix the timing math, then worry about the animation curve. The component is the easy part. The discipline about when to use it is the whole job.
If you want a second set of eyes on your notification patterns or a broader UX audit, our design and product team does this kind of work often — and the blog has more on accessibility and design-system detail if you want to keep reading.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a projectKeep reading

Design Tokens That Survive Contact With Engineering: A Naming Convention That Scales
Most design token systems die at the second theme. Here's a three-tier naming convention that survives dark mode, brand refreshes, and the inevitable Tailwind migration.

Empty States Are Your Second Homepage: Designing Them Like Product Surface
Empty states get treated like an afterthought — a shrug icon and a sentence. They're actually one of the highest-leverage screens in your product. Here's how to design and ship them like it.

Skeleton Screens Are Lying to Your Users: A Better Loading Strategy
Skeleton screens became the default for perceived performance, but most implementations make apps feel slower and less trustworthy. Here's when to use them, when to skip them, and what to build instead.
