TL;DR: Sonner is an opinionated toast library with polished default styles. One
<Toaster />in your app root, then calltoast()from anywhere - no hooks, no context. Handles stacking, hover-to-expand, and swipe-to-dismiss. ~4.4kB. Official docs: sonner.emilkowal.ski.
What is Sonner?
Sonner is a toast notification library with an extremely minimal API and polished default styles. You render one <Toaster /> in your app, then call toast() from anywhere - no context, no hooks, no state management required. It handles stacking, hover-to-expand, and swipe-to-dismiss out of the box.
I switched a form-heavy admin panel from a hand-rolled toast context (about 90 lines of provider, reducer, and portal code) to Sonner in under half an hour, and deleted all of it the same afternoon. The stacking behavior alone was worth the swap - our old implementation just piled new toasts on top of old ones until the corner of the screen turned into a wall of unreadable text.
When to use it
Use Sonner when you want great-looking toasts with zero configuration. It's the default toast library in shadcn/ui and handles 90% of use cases elegantly.
Sonner vs React Hot Toast: Sonner has better default animations and stacking behaviour. React Hot Toast is slightly smaller and has been around longer. Both are excellent - Sonner has more momentum in 2024 - 2025.
Key Features
- Stack animation - multiple toasts stack with a fan effect; hover to expand
- Toast types -
toast.success(),toast.error(),toast.warning(),toast.info() - Promise toasts -
toast.promise()auto-transitions through loading → success/error - Rich content - render custom JSX inside a toast
- Position - 6 positions including
top-centerandbottom-right
Installation
npm install sonner
Basic Setup
// app/layout.tsx or root component
import { Toaster } from 'sonner'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{children}
<Toaster position="bottom-right" richColors />
</body>
</html>
)
}
Usage
import { toast } from 'sonner'
function SaveButton() {
async function handleSave() {
toast.promise(saveData(), {
loading: 'Saving...',
success: 'Changes saved',
error: 'Failed to save',
})
}
return <button onClick={handleSave}>Save</button>
}
// Inline usage anywhere:
toast.success('Profile updated')
toast.error('Something went wrong', { description: 'Please try again later.' })
toast.warning('Unsaved changes', { action: { label: 'Save now', onClick: save } })
Custom Toast with Action
toast('File deleted', {
description: 'document.pdf has been removed',
action: {
label: 'Undo',
onClick: () => restoreFile(),
},
duration: 5000,
})
TypeScript Tips
Sonner is fully typed. The toast() return value is the toast ID - use it to dismiss or update a toast programmatically:
const id = toast.loading('Processing...')
try {
await processData()
toast.success('Done!', { id }) // replaces the loading toast
} catch {
toast.error('Failed', { id })
}
Limiting How Many Toasts Stack at Once
By default Sonner shows 3 stacked toasts before collapsing the rest into the pile. On a page that fires a lot of background notifications (bulk import progress, websocket events), that default can still feel noisy. visibleToasts and a shared max-count guard keep it in check:
<Toaster visibleToasts={2} />
import { toast } from 'sonner'
let activeImportToastId: string | number | undefined
function notifyImportProgress(done: number, total: number) {
const message = `Imported ${done} of ${total} rows`
activeImportToastId = toast.loading(message, { id: activeImportToastId })
}
Reusing the same id on every call replaces the existing toast in place instead of stacking a new one for every progress tick, which matters a lot once an import fires updates every few hundred milliseconds.
Common Gotchas
- One
<Toaster />per app - render it once at the root; multipleToasterinstances conflict richColorsprop - add this to<Toaster>to get coloured backgrounds for success/error/warning toasts instead of monochrome- Duration - default duration is 4000ms. Override globally on
<Toaster duration={6000}>or per-toast withtoast('msg', { duration: Infinity }) - Server components -
toast()is a client-side call; call it from event handlers or client components, not server actions directly
Related
- React Hot Toast - the alternative toast library; slightly older, fully customisable render function, similar API
- React 19 Guide - call
toast()after awaiting a React 19 Action to surface server mutation results in the UI
Server Actions Integration (Next.js)
toast() is a client-side call. The recommended pattern with React 19 Actions or Next.js Server Actions is to call it in the client after the action resolves:
'use client'
import { toast } from 'sonner'
import { updateProfile } from '@/actions/profile'
function ProfileForm() {
async function handleSubmit(formData: FormData) {
const result = await updateProfile(formData)
if (result.error) {
toast.error(result.error)
} else {
toast.success('Profile updated')
}
}
return <form action={handleSubmit}>...</form>
}
Theming and Dark Mode
Sonner respects the theme prop and automatically adapts to dark mode:
<Toaster theme="system" /> {/* auto: light/dark based on prefers-color-scheme */}
<Toaster theme="dark" />
<Toaster theme="light" />
Pass richColors together with theme for coloured success/error/warning backgrounds that stay readable in both modes. The stacking limit (default 3 visible toasts) can be changed via <Toaster visibleToasts={5} />. Pass a custom icon per toast call to override the default success/error icons.