
Toasts Nobody Hates
Toast notifications are usually an accessibility failure with a nice animation. Getting them right is mostly about knowing when not to use one.
A toast is a message that appears in a corner and leaves on a timer. Which means it is, by construction, a bad place to put anything the user needs — they may be looking elsewhere, they may read slowly, they may be using a screen reader that was mid-sentence.
Most toast bugs are not implementation bugs. They are the decision to use a toast at all.
When a toast is right
- Confirming something the user just did, where the result is also visible elsewhere. "Saved" next to a list that now contains the item.
- Low-stakes background news: "Copied to clipboard".
When it is wrong
- Form validation errors. These go next to the field, tied with
aria-describedby. A toast makes the user hunt for which of eleven fields is wrong. - Anything with an action that has no other home. An "Undo" that only exists in a toast is an undo that vanishes after four seconds.
- Errors the user must act on. Those need to persist until dismissed, in context.
Getting this list right matters more than which library you pick.
Then, the implementation
import { Toaster, toast } from 'sonner'
// once, near the root
<Toaster position="bottom-right" closeButton richColors />
toast.success('Project saved')
toast.error('Could not reach the server', { duration: Infinity })
Note the second one: an error the user needs to see does not get a timer.
The promise helper covers the common async triple without three call sites:
toast.promise(saveProject(data), {
loading: 'Saving…',
success: 'Project saved',
error: (e) => `Could not save: ${e.message}`,
})
The accessibility part
A toast that renders into a properly configured live region gets announced. One that does not is invisible to a screen-reader user — and if it was your only confirmation that a destructive action succeeded, that user is now guessing.
Sonner handles the live region, focus behaviour and hover-to-pause. What it cannot do is stop you putting essential information in a transient element. Assume every toast will be missed by someone, and make sure nothing important only exists there.
Duration, honestly
Four seconds is roughly two seconds of reading for a message in your second language. On bilingual products I raise the default and add a close button, because a dismissible toast that stays put is strictly better than one that races the reader.
Resources
- Repo: emilkowalski/sonner
- Docs: sonner.emilkowal.ski
- Video walkthroughs: YouTube: sonner react toast tutorial
- Related: React Hook Form: fewer renders, fewer bugs
Need this built properly?
I build secure, fast, bilingual platforms for clients across Egypt, Saudi Arabia, the UAE and Kuwait.


