Toasts solve a precise UX problem: the user does something, you need to tell them what happened, and you have two seconds before they stop caring. A modal is overkill. A spinner is meaningless without context. A status bar buried in the header is invisible. A toast appears, delivers the message, and disappears. No interaction required, no scroll hunt, no confusion. It's the JavaScript equivalent of a notification badge on your iPhone—immediate, non-blocking, done.
The trap most teams fall into is building toasts from scratch. Custom positioning logic, custom animations, custom stack management, custom accessibility. Months later, you're debugging why toasts overlap on mobile, why the 8th toast breaks the layout, why keyboard users can't dismiss them. Use a library. Two options dominate: Sonner (modern, opinionated, beautiful) and react-hot-toast (lightweight, flexible, battle-tested). Both are free. Both take 5 minutes to set up. The choice matters less than the decision to use one.
Sonner vs react-hot-toast: The Quick Comparison
Sonner is the new hotness. Beautiful UI out of the box. Dark mode and light mode handled automatically. Toasts stack with zero config. Rich support for promises, loading states, and custom components. Small bundle hit. Aidxn ships this on every Velocity dashboard. The trade-off: less customization—if you want full design control, Sonner might feel opinionated.
react-hot-toast is the Swiss Army knife. Smaller, simpler, extremely flexible. You can theme it however you want. Lighter bundle if you're cutting bytes. The trade-off: you'll write more code. Stack management is manual. Dark mode is up to you. Promise support requires a bit of setup.
Pick Sonner if: you want beautiful defaults, dark mode out of the box, and zero config stack management. Pick react-hot-toast if: you need minimal footprint or extreme design control.
Sonner Setup: 30 Seconds, Start Using
npm install sonner
Wrap your app in the Toaster component:
// src/layouts/Layout.astro
import { Toaster } from 'sonner';
// Somewhere in your Astro layout, add as a React island:
<Toaster
position="bottom-right"
theme="auto"
richColors
closeButton
/>
Now use it anywhere in a React component:
import { toast } from 'sonner';
export function DeleteButton({ itemId }) {
const handleDelete = async () => {
// Show success immediately
toast.success('Item deleted');
// Or: show while a promise resolves
toast.promise(
fetch(`/api/items/${itemId}`, { method: 'DELETE' }),
{
loading: 'Deleting...',
success: 'Item deleted',
error: 'Failed to delete',
}
);
};
return <button onClick={handleDelete}>Delete</button>;
}
That's it. No config, no stacking logic, no theme overrides. Sonner handles everything. Toasts appear in the bottom-right, auto-dismiss after 4 seconds, stack vertically on mobile, dark mode switches with your OS. Ship it.
react-hot-toast Setup: Slightly More Config
npm install react-hot-toast
// src/components/layout/Toaster.tsx
import { Toaster } from 'react-hot-toast';
export function AppToaster() {
return (
<Toaster
position="bottom-right"
reverseOrder={false}
toastOptions={{
duration: 4000,
style: {
background: '#363636',
color: '#fff',
},
success: {
duration: 3000,
style: {
background: '#10b981',
},
},
error: {
duration: 4000,
style: {
background: '#ef4444',
},
},
}}
/>
);
}
Use it the same way:
import toast from 'react-hot-toast';
export function DeleteButton({ itemId }) {
const handleDelete = () => {
toast.success('Item deleted');
};
return <button onClick={handleDelete}>Delete</button>;
}
With react-hot-toast, you're writing more setup code upfront to gain design flexibility. If your brand colors are purple and teal, react-hot-toast lets you bake that into the config. Sonner ships good defaults and you're done.
Four Toast Patterns You'll Use Every Day
Pattern 1: Simple Success Toast
import { toast } from 'sonner';
export function SaveButton() {
const handleSave = () => {
toast.success('Changes saved');
};
return <button onClick={handleSave}>Save</button>;
}
User clicks, toast appears, user sees confirmation. No spinner, no modal, no focus steal. Context preserved. This is the default pattern.
Pattern 2: Error Toast with Action
import { toast } from 'sonner';
export function DeleteButton({ itemId }) {
const handleDelete = async () => {
try {
await fetch(`/api/items/${itemId}`, { method: 'DELETE' });
toast.success('Item deleted');
} catch (err) {
toast.error('Failed to delete. Try again.', {
action: {
label: 'Retry',
onClick: () => handleDelete(),
},
});
}
};
return <button onClick={handleDelete}>Delete</button>;
}
Error toast with a clickable action. User sees the error and can retry without reaching for another button. Sonner handles the layout; you just pass the action config.
Pattern 3: Promise Toast (Loading → Success/Error)
import { toast } from 'sonner';
export function SaveButton() {
const handleSave = async () => {
toast.promise(
fetch('/api/save', {
method: 'POST',
body: JSON.stringify({ /* data */ }),
}).then(r => r.json()),
{
loading: 'Saving...',
success: 'Saved successfully',
error: 'Failed to save',
}
);
};
return <button onClick={handleSave}>Save</button>;
}
One toast, three states. User sees "Saving..." while the promise is pending, "Saved successfully" on success, "Failed to save" on error. No spinner UI, no manual state tracking. The promise drives it all.
Pattern 4: Loading Toast (Long-Running Task)
import { toast } from 'sonner';
export function ImportButton() {
const handleImport = async () => {
const toastId = toast.loading('Importing...');
try {
const result = await fetch('/api/import', { method: 'POST' }).then(r => r.json());
toast.success('Imported 500 items', { id: toastId });
} catch (err) {
toast.error('Import failed', { id: toastId });
}
};
return <button onClick={handleImport}>Import CSV</button>;
}
Long-running operations (CSV imports, batch processing, file uploads) show a loading toast first, then update it with the final result. The `id` parameter lets you replace the same toast instead of stacking new ones. User sees one notification, not three.
Accessibility & Mobile Considerations
Toasts are non-blocking, which means accessibility tools and keyboard users might miss them. Sonner includes ARIA live regions by default, so screen readers announce toasts. Add `role="alert"` if you're building custom toasts. Dismiss buttons are important—users should always be able to close a toast if they need focus.
Mobile is the tricky part. Toasts on the bottom-right work on desktop but collide with mobile keyboard on some devices. Both Sonner and react-hot-toast offer `position="top-center"` or `position="bottom-center"`. Test your toasts on a real phone before shipping. A toast that hides behind the keyboard is worse than no toast. Sonner's position prop handles most cases; react-hot-toast requires more testing.
Six FAQs
Should I always show a success toast?
Only if the success isn't obvious from the UI. Clicking "Like" and seeing the heart fill red—no toast needed. Running an import and seeing the count jump—no toast needed. But if the feedback is async and non-visual, toast it.
How long should a toast stay on screen?
Default 4 seconds. Errors should stay longer (6–8 seconds) so users have time to read and act. Loading states should persist until the action completes. Let Sonner/react-hot-toast handle defaults, override only when necessary.
Can I stack toasts vertically?
Yes, both libraries do this by default. On mobile, they're usually centered or bottom-centered and take full width. Test on your target devices. Too many toasts at once (4+) is a sign you should batch notifications or use a different UX pattern.
What if I need a custom toast design?
Sonner supports custom components: `toast.custom((t) => <MyToast />)`. react-hot-toast does the same. But if you're reaching for full custom design, consider whether a side panel or modal would be clearer. Toasts are for quick feedback, not complex layouts.
Do toasts work in dark mode?
Sonner: yes, automatic with `theme="auto"`. react-hot-toast: you set the colors manually. See the pattern above. If your app respects `prefers-color-scheme`, your toasts should too.
Can I use toasts on every action or is that spam?
Spam if every click shows a toast. Smart if toasts provide genuine feedback. Deleting something: yes, toast it. Opening a dropdown: no. Submitting a form: yes. Clicking a navigation link: no. Use toasts as a signal to the user that something async happened or something requires attention.
The Bottom Line
Toasts are free UX wins. Pick Sonner for beautiful defaults and zero config, or react-hot-toast for control and flexibility. Either way, you'll ship notifications in 5 minutes instead of weeks. Support success, error, loading, and promise states. Test on mobile. Keep them short and actionable. Use them as confirmation, not distraction. And if you're building a Velocity dashboard, start with Sonner—it's what optimistic UI updates deserve. Ship it.