Fix: Sonner Not Working — Toasts Not Showing, Styling Broken, or Server Component Errors
Quick Answer
How to fix Sonner toast notification issues — Toaster setup, toast types, custom styling, promise toasts, action buttons, positioning, dark mode, and Next.js App Router integration.
The Problem
toast() is called but no notification appears:
import { toast } from 'sonner';
function handleSave() {
toast('Document saved');
// Nothing visible on screen
}Or toasts render but with broken styling — no background, no border, invisible text:
Toast text appears but is transparent or overlaps other contentOr calling toast() in a Server Component throws:
Error: toast is not a function — or — Cannot call client function from serverWhy This Happens
Sonner is a toast notification library for React that renders notifications from a central <Toaster /> component:
<Toaster />must be mounted in the component tree —toast()queues notifications, but they’re rendered by the<Toaster />component. Without it in your layout, notifications have nowhere to render. This is the most common issue.- Sonner ships minimal default styles — the toast styles depend on Sonner’s CSS being loaded. Some bundler configurations or CSS-in-JS setups strip the styles. The
<Toaster />component accepts athemeprop and custom className overrides. toast()is a client-side function — it can’t be called directly in Server Components or Server Actions. Trigger toasts from client components or pass success/error state from server to client.
Fix 1: Basic Setup
npm install sonner// app/layout.tsx — mount Toaster once at the root
import { Toaster } from 'sonner';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{children}
<Toaster />
</body>
</html>
);
}// Any client component — trigger toasts
'use client';
import { toast } from 'sonner';
function SaveButton() {
function handleSave() {
// Basic toast
toast('Document saved successfully');
// Success toast
toast.success('Profile updated');
// Error toast
toast.error('Failed to save changes');
// Warning toast
toast.warning('Your session is about to expire');
// Info toast
toast.info('New version available');
// With description
toast.success('File uploaded', {
description: 'resume.pdf has been uploaded to your documents',
});
// With duration (milliseconds)
toast('Quick notification', { duration: 2000 });
// Persistent toast (no auto-dismiss)
toast('Important message', { duration: Infinity });
}
return <button onClick={handleSave}>Save</button>;
}Fix 2: Promise Toasts (Loading → Success/Error)
'use client';
import { toast } from 'sonner';
function SubmitForm() {
async function handleSubmit() {
// Automatically shows loading, then success or error
toast.promise(
fetch('/api/submit', { method: 'POST', body: JSON.stringify(data) })
.then(res => {
if (!res.ok) throw new Error('Failed');
return res.json();
}),
{
loading: 'Submitting...',
success: (data) => `Saved: ${data.name}`,
error: (err) => `Error: ${err.message}`,
}
);
}
// Or with async function
async function handleUpload(file: File) {
toast.promise(uploadFile(file), {
loading: `Uploading ${file.name}...`,
success: 'Upload complete!',
error: 'Upload failed. Please try again.',
});
}
return <button onClick={handleSubmit}>Submit</button>;
}Fix 3: Action Buttons and Custom Content
'use client';
import { toast } from 'sonner';
function NotificationExamples() {
// Toast with action button
function handleDelete() {
const itemId = '123';
toast('Item deleted', {
action: {
label: 'Undo',
onClick: () => restoreItem(itemId),
},
duration: 5000,
});
deleteItem(itemId);
}
// Toast with cancel button
function handleArchive() {
toast('Archive this conversation?', {
action: {
label: 'Archive',
onClick: () => archiveConversation(),
},
cancel: {
label: 'Cancel',
onClick: () => {},
},
});
}
// Custom JSX content
function showCustomToast() {
toast.custom((t) => (
<div className="flex items-center gap-3 bg-white border rounded-lg p-4 shadow-lg">
<img src="/avatar.jpg" className="w-10 h-10 rounded-full" />
<div>
<p className="font-medium">New message from Alice</p>
<p className="text-sm text-gray-500">Hey, are you available?</p>
</div>
<button
onClick={() => toast.dismiss(t)}
className="ml-auto text-gray-400 hover:text-gray-600"
>
✕
</button>
</div>
));
}
// Dismiss specific or all toasts
function dismissExamples() {
const toastId = toast('Dismissable toast');
toast.dismiss(toastId); // Dismiss specific toast
toast.dismiss(); // Dismiss all toasts
}
return (
<div>
<button onClick={handleDelete}>Delete</button>
<button onClick={handleArchive}>Archive</button>
<button onClick={showCustomToast}>Custom</button>
</div>
);
}Fix 4: Toaster Configuration and Styling
import { Toaster } from 'sonner';
// Full configuration
<Toaster
position="bottom-right" // top-left, top-center, top-right,
// bottom-left, bottom-center, bottom-right
expand={false} // Expand toasts on hover
richColors // Enhanced colors for success/error/warning
closeButton // Show close button on all toasts
duration={4000} // Default duration (ms)
theme="system" // 'light' | 'dark' | 'system'
visibleToasts={3} // Max visible at once
offset="16px" // Distance from viewport edge
dir="ltr" // Text direction
// Custom styles
toastOptions={{
className: 'my-toast',
style: {
background: '#1a1a2e',
color: '#ffffff',
border: '1px solid #333',
},
// Per-type styling
classNames: {
success: 'bg-green-50 border-green-200 text-green-800',
error: 'bg-red-50 border-red-200 text-red-800',
warning: 'bg-yellow-50 border-yellow-200 text-yellow-800',
info: 'bg-blue-50 border-blue-200 text-blue-800',
},
}}
/>
// Tailwind dark mode
<Toaster
theme="system"
toastOptions={{
classNames: {
toast: 'bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700',
title: 'text-gray-900 dark:text-gray-100',
description: 'text-gray-500 dark:text-gray-400',
actionButton: 'bg-blue-500 text-white',
cancelButton: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300',
},
}}
/>Fix 5: Server Action Integration
// app/actions.ts
'use server';
export async function updateProfile(formData: FormData) {
const name = formData.get('name') as string;
try {
await db.update(users).set({ name }).where(eq(users.id, currentUser.id));
return { success: true, message: 'Profile updated' };
} catch {
return { success: false, message: 'Failed to update profile' };
}
}// components/ProfileForm.tsx
'use client';
import { toast } from 'sonner';
import { updateProfile } from '@/app/actions';
function ProfileForm() {
async function handleSubmit(formData: FormData) {
const result = await updateProfile(formData);
if (result.success) {
toast.success(result.message);
} else {
toast.error(result.message);
}
}
return (
<form action={handleSubmit}>
<input name="name" />
<button type="submit">Save</button>
</form>
);
}
// Or use toast.promise with server action
function PromiseForm() {
function handleSubmit(formData: FormData) {
toast.promise(updateProfile(formData), {
loading: 'Saving...',
success: (result) => result.message,
error: 'Something went wrong',
});
}
return (
<form action={handleSubmit}>
<input name="name" />
<button type="submit">Save</button>
</form>
);
}Fix 6: Headless Mode and Testing
// Headless — no default styles, full control
import { toast, Toaster } from 'sonner';
<Toaster
toastOptions={{
unstyled: true, // Remove all default styles
classNames: {
toast: 'flex items-center gap-2 p-4 rounded-xl shadow-xl border',
title: 'font-semibold',
description: 'text-sm opacity-70',
actionButton: 'px-3 py-1 rounded bg-blue-500 text-white text-sm',
cancelButton: 'px-3 py-1 rounded bg-gray-200 text-sm',
success: 'bg-green-50 border-green-200',
error: 'bg-red-50 border-red-200',
},
}}
/>
// Multiple Toaster instances (different positions)
// Top for errors, bottom for success
<Toaster position="top-center" toastOptions={{ classNames: { error: 'top-toast' } }} />
// Note: Sonner supports only one Toaster — use toast options to differentiateStill Not Working?
Toast is called but nothing appears — <Toaster /> is missing from your component tree. Add it to your root layout. It only needs to be mounted once. Verify by adding <Toaster /> directly next to the button that calls toast() — if it works there but not in the layout, there’s a rendering issue with your layout component.
Toasts appear but are invisible (transparent) — Sonner’s styles might not be loading. Add richColors to the <Toaster /> for more visible default styles. If using Tailwind, Sonner’s default styles may conflict. Use the toastOptions.classNames to apply Tailwind classes explicitly.
toast() in Server Action doesn’t work — toast() is client-side only. Server Actions run on the server. Return a result from the action, then call toast() in the client component based on that result. The toast.promise() pattern works because the promise resolves on the client.
Toasts stack indefinitely — set visibleToasts={3} on <Toaster /> to limit visible toasts. Excess toasts queue and appear as previous ones dismiss. Also set a reasonable duration — the default is 4000ms.
For related UI issues, see Fix: Radix UI Not Working and Fix: shadcn/ui Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: cmdk Not Working — Command Palette Not Opening, Items Not Filtering, or Keyboard Navigation Broken
How to fix cmdk command palette issues — Dialog setup, custom filtering, groups and separators, keyboard shortcuts, async search, nested pages, and integration with shadcn/ui and Tailwind.
Fix: Conform Not Working — Form Validation Not Triggering, Server Errors Missing, or Zod Schema Rejected
How to fix Conform form validation issues — useForm setup with Zod, server action integration, nested and array fields, file uploads, progressive enhancement, and Remix and Next.js usage.
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.
Fix: Lingui Not Working — Messages Not Extracted, Translations Missing, or Macro Errors
How to fix Lingui.js i18n issues — setup with React, message extraction, macro compilation, ICU format, lazy loading catalogs, and Next.js integration.