Sonner: The React Toast Library You’ll Actually Enjoy Using
React toast notifications
React notification library
sonner tutorial
sonner customization
sonner promise
React toast hooks
React alert notifications
toast component React
Why Toast Notifications Are Still Worth Getting Right
Toast notifications are one of those UI elements that look simple until you actually try to build them well. Positioning, stacking, animation, accessibility, theming, promise states — each one is a rabbit hole. Most developers reach for a library, find something that kind of works, wrestle with its CSS overrides for two hours, and ship something that quietly embarrasses the whole team. That story ends with Sonner.
Sonner is an opinionated, beautifully minimal React toast notification library built by Emil Kowalski. It was designed with one goal: to give developers production-quality toast messages with almost zero configuration. No CSS imports. No wrapping your entire app in a provider. No reading a 40-page docs page to render a single notification. Just install, drop in a component, and call toast().
The library has earned serious traction in the React ecosystem — partly because it integrates seamlessly with shadcn/ui, but mostly because it actually looks good and behaves the way users expect. If you’ve been tolerating a mediocre React notification system, this tutorial will give you a concrete reason to make the switch.
Sonner Installation and Initial Setup
Getting Sonner running takes about 90 seconds. That’s not marketing copy — it’s just how the library is designed. Start by installing it from npm (or your package manager of choice):
# npm
npm install sonner
# pnpm
pnpm add sonner
# yarn
yarn add sonner
Once installed, open your application root — App.jsx, App.tsx, or layout.tsx in Next.js App Router — and render the <Toaster /> component. This is the single mount point for every toast notification in your entire application. No context. No wrapping. One line:
// App.tsx (or your root layout)
import { Toaster } from 'sonner';
export default function App() {
return (
<>
<Toaster />
{/* rest of your app */}
</>
);
}
From any component — any depth in the tree, any file — you can now import the toast function and fire a notification. It doesn’t need to know about the Toaster component’s location. It just works:
import { toast } from 'sonner';
function SaveButton() {
return (
<button onClick={() => toast('Changes saved successfully.')}>
Save
</button>
);
}
toast() inside it directly. The <Toaster /> component itself needs 'use client' in App Router environments, but you can isolate it in a small wrapper to keep your layout server-friendly.
Core Toast Types: From Alerts to Actions
Sonner ships with several built-in toast variants that cover virtually every React alert notification scenario you’ll encounter in real projects. Each one is a method on the toast object, with sensible default styling and iconography out of the box.
import { toast } from 'sonner';
// Basic message
toast('Profile updated.');
// Semantic variants
toast.success('Payment confirmed!');
toast.error('Something went wrong. Please try again.');
toast.warning('Your session expires in 5 minutes.');
toast.info('A new version is available.');
// Custom description
toast('Email sent', {
description: 'Your message has been delivered to the recipient.',
});
// With an action button
toast('File deleted', {
action: {
label: 'Undo',
onClick: () => restoreFile(),
},
});
The action toast pattern deserves special mention. The ability to embed an undo action directly into a toast notification — without building a custom component or managing extra state — is one of those features that would take a full afternoon to implement from scratch. Sonner makes it a four-line configuration object. This pattern is especially powerful for destructive operations: deleting records, archiving items, or bulk-updating data.
There’s also toast.loading(), which returns a toast ID you can use to later update or dismiss that specific notification programmatically. This gives you fine-grained control over long-running processes without managing a pile of boolean state or juggling toast lifecycles manually.
const toastId = toast.loading('Uploading files...');
// Later, after your async operation resolves:
toast.success('Upload complete!', { id: toastId });
// Or on failure:
toast.error('Upload failed. Check your connection.', { id: toastId });
Sonner Promise Toasts: Async Made Elegant
If there’s one feature that makes developers genuinely enthusiastic about Sonner, it’s toast.promise(). Handling async operations in UI is almost always messier than it should be — you need loading indicators, success messages, error states, and you need them to not fight each other visually. The promise toast pattern solves this with a single, declarative API.
import { toast } from 'sonner';
async function submitOrder(orderData) {
toast.promise(
fetch('/api/orders', {
method: 'POST',
body: JSON.stringify(orderData),
}).then(res => res.json()),
{
loading: 'Placing your order...',
success: (data) => `Order #${data.orderId} confirmed!`,
error: (err) => `Failed to place order: ${err.message}`,
}
);
}
Notice that the success and error handlers accept a callback, giving you access to the resolved value or rejection reason. This means your toast messages can be dynamic, pulling real data from the response — order numbers, usernames, file names — rather than generic strings. The toast automatically transitions through the loading → resolved/rejected states with smooth animations and zero additional logic on your part.
The promise toast also pairs naturally with libraries like React Query or SWR. You can wrap a mutation function in toast.promise() and let Sonner handle the entire notification lifecycle while your data-fetching library handles the actual state management. No prop drilling, no context gymnastics — just a clean separation of concerns that scales well in larger React notification systems.
Customization: Making Sonner Fit Your Design System
Sonner’s defaults are good enough to ship as-is, but real projects have design systems. Fortunately, the library’s customization model is coherent and layered — you can customize globally via the <Toaster /> component props, or override per-toast via the options object. Both approaches use the same API surface, which means less cognitive overhead when switching between them.
<Toaster
position="bottom-right"
expand={true}
richColors={true}
duration={4000}
closeButton={true}
theme="dark"
toastOptions={{
style: {
background: '#1e1e3f',
color: '#e2e8f0',
border: '1px solid #6366f1',
borderRadius: '8px',
},
className: 'my-toast',
}}
/>
The richColors prop enables Sonner’s full semantic color palette — meaningful greens for success, reds for errors, yellows for warnings — rather than the minimal monochrome default. The expand prop controls whether stacked toasts expand on hover (the Stack design pattern, popularized by Vercel’s dashboard). Both are single boolean props that would take hours to implement correctly from scratch.
For deeper brand integration, Sonner supports custom toast components. If your design system requires a completely custom layout — a toast with a user avatar, a progress bar, or a multi-line rich notification — you can pass JSX directly to toast.custom():
toast.custom((t) => (
<div className="flex items-center gap-3 p-4 bg-white rounded-xl shadow-lg">
<img src="/avatar.png" alt="User" className="w-10 h-10 rounded-full" />
<div>
<p className="font-semibold">New follower</p>
<p className="text-sm text-gray-500">@emilkowalski started following you</p>
</div>
<button onClick={() => toast.dismiss(t)}>✕</button>
</div>
));
The callback receives the toast ID (t), which you can use to programmatically dismiss it — as shown in the close button above. This pattern gives you the full power of React rendering inside a toast, while Sonner still handles positioning, stacking, animation, and accessibility. It’s a thoughtful escape hatch that doesn’t feel like a bolt-on.
React Toast Hooks and Programmatic Control
Beyond the basic toast() API, Sonner exposes toast.dismiss() for dismissing specific toasts by ID, and works well with React’s standard hook patterns. Since toast is a plain function (not a hook), you can call it anywhere — event handlers, utility functions, service layers, even outside React components entirely. This architectural decision is surprisingly important in large applications where notification logic lives in non-component code.
// In an API service file (no React component needed)
import { toast } from 'sonner';
export async function deleteUser(userId: string) {
const id = toast.loading('Removing user account...');
try {
await api.delete(`/users/${userId}`);
toast.success('Account removed.', { id });
} catch (error) {
toast.error('Could not remove account. Try again.', { id });
}
}
If you want hook-based patterns for React-specific contexts, you can wrap Sonner’s API in a custom hook for consistent toast messaging across your application. This is particularly useful for enforcing consistent messaging, duration, and style standards without repeating configuration in every component:
// hooks/useAppToast.ts
import { toast } from 'sonner';
export function useAppToast() {
const success = (message: string) =>
toast.success(message, { duration: 3000 });
const error = (message: string) =>
toast.error(message, { duration: 5000, closeButton: true });
const async = <T,>(promise: Promise<T>, messages: {
loading: string;
success: string;
error: string;
}) => toast.promise(promise, messages);
return { success, error, async };
}
This pattern centralizes your React notification system configuration in one place, making it trivial to update styling or behavior application-wide. It also makes testing easier — you can mock useAppToast cleanly rather than mocking the underlying Sonner internals in every test file.
Accessibility, Performance, and Real-World Considerations
Sonner is built with accessibility as a default, not an afterthought. The <Toaster /> component renders an ARIA live region with the appropriate role="status" or role="alert" depending on toast type. Screen readers announce toast messages without interrupting the user’s current focus or workflow — a detail that most homegrown toast implementations completely miss. For toast.error(), Sonner uses aria-live="assertive" to ensure critical messages are announced immediately.
From a performance standpoint, Sonner’s bundle size is remarkably lean — under 3KB gzipped. It uses CSS animations rather than a JavaScript animation library, keeping the runtime overhead minimal. Toasts are portaled to document.body by default, which means they’re removed from your component tree’s rendering concerns entirely. High-frequency interactions — rapid successive clicks, real-time event streams — won’t degrade your application’s frame rate because Sonner queues and throttles notifications intelligently.
One real-world gotcha: if you’re using Sonner with React Server Components in Next.js App Router, remember that toast() can only be called in Client Components. The <Toaster /> mount point should be isolated in a small 'use client' wrapper file to avoid polluting your server layout. Server Actions, however, can trigger toasts indirectly by passing results to client-side handlers — a pattern that works well with Next.js’s form action model.
Sonner vs. React-Toastify: The Honest Comparison
The most common question developers ask before adopting any React toast library is: « But I already use react-toastify — should I switch? » The answer depends on your project’s priorities, but the tradeoffs are clear. React-toastify is the incumbent — battle-tested, with a massive install base and deep configuration options. It also requires a CSS import, has a larger bundle footprint, and its default aesthetics feel dated compared to modern design standards.
Sonner wins decisively on developer experience, visual quality, and modern React compatibility. It has zero required CSS imports, first-class TypeScript support, and a composable API that fits naturally into today’s React patterns. Its advanced features like promise toasts and custom components are genuinely elegant rather than bolted-on. If you’re starting a new project, or if your design team has been quietly cringing at your current notifications, Sonner is the clear choice.
- Bundle size: Sonner ~3KB gzipped vs. react-toastify ~11KB gzipped
- CSS required: Sonner — no. react-toastify — yes (
ReactToastify.css) - Promise toasts: Sonner — native, elegant. react-toastify — available but verbose
- Default aesthetics: Sonner — polished, modern. react-toastify — functional, dated
- Next.js App Router: Sonner — works cleanly. react-toastify — requires careful setup
- Custom components: Both support, Sonner’s API is cleaner
That said, if you have thousands of lines of react-toastify code in a legacy codebase and a deadline tomorrow, Sonner can wait. Migration is straightforward but not zero-effort. The API surfaces differ enough that a direct find-replace won’t cut it — you’ll need to spend an afternoon updating toast calls and configuration. For greenfield projects or dedicated refactoring sprints, that investment pays off quickly.
A Complete Sonner Example: Real-World Integration
Let’s tie everything together with a realistic example — a file upload form that handles loading, success, and error states using Sonner’s full feature set. This is the kind of pattern you’ll actually use in production, not a contrived « click button, see toast » demo.
// components/FileUploader.tsx
'use client';
import { useState } from 'react';
import { toast } from 'sonner';
async function uploadFile(file: File): Promise<{ url: string }> {
const formData = new FormData();
formData.append('file', file);
const res = await fetch('/api/upload', { method: 'POST', body: formData });
if (!res.ok) throw new Error(await res.text());
return res.json();
}
export function FileUploader() {
const [isDragging, setIsDragging] = useState(false);
function handleFile(file: File) {
if (file.size > 10 * 1024 * 1024) {
toast.error('File too large', {
description: 'Maximum upload size is 10MB.',
duration: 5000,
});
return;
}
toast.promise(uploadFile(file), {
loading: `Uploading ${file.name}...`,
success: (data) => ({
message: 'Upload complete',
description: `File available at: ${data.url}`,
action: {
label: 'Copy URL',
onClick: () => navigator.clipboard.writeText(data.url),
},
}),
error: (err) => `Upload failed: ${err.message}`,
});
}
return (
<div
onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
onDragLeave={() => setIsDragging(false)}
onDrop={(e) => {
e.preventDefault();
setIsDragging(false);
const file = e.dataTransfer.files[0];
if (file) handleFile(file);
}}
className={`upload-zone ${isDragging ? 'dragging' : ''}`}
>
<p>Drag a file here or <label>
browse
<input
type="file"
hidden
onChange={(e) => e.target.files?.[0] && handleFile(e.target.files[0])}
/>
</label></p>
</div>
);
}
Notice how the success callback returns an object with message, description, and action — giving you rich success notifications without any additional complexity. The file size validation fires a simple error toast before even attempting the upload. The entire notification lifecycle — validation error, upload loading state, success with an action, or error with the server message — is handled in roughly 15 lines of notification-specific code. That’s the productivity improvement Sonner delivers in practice.
This pattern is also easy to extract into a reusable hook or utility. Because toast.promise() returns the toast ID, you could build more sophisticated orchestration — cancellation buttons, multi-file upload progress tracking, or chained async operations — without fighting the library’s assumptions. Sonner gives you guard rails without building a cage.
Frequently Asked Questions
How do I install and set up Sonner in a React project?
Run npm install sonner (or pnpm add sonner / yarn add sonner). Then add <Toaster /> from 'sonner' to your application root — once, anywhere high in the tree. After that, import toast from 'sonner' in any component and call it directly. No provider wrapping, no CSS imports, no additional configuration required to get your first toast working.
Can Sonner handle async or promise-based toast notifications?
Yes — and this is one of its strongest features. toast.promise() accepts any Promise and transitions the notification through loading → success → error states automatically. The success and error handlers accept either a string or a callback that receives the resolved value or rejection reason, so your notifications can display dynamic, context-aware messages from your API responses. It requires zero additional state management on your end.
How does Sonner compare to react-toastify?
Sonner is approximately 4× smaller (≈3KB vs ≈11KB gzipped), requires no CSS imports, has better default aesthetics, and integrates more naturally with modern React patterns including Next.js App Router and TypeScript. React-toastify has a larger ecosystem and more historical configuration options, making it more familiar for teams with existing implementations. For new projects, Sonner is the stronger choice on almost every practical dimension — DX, visual quality, bundle impact, and accessibility defaults.