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.
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 Server Actions 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.