Sonner
A Sonner toaster with the Depth surface of the buttons and tinted status icons.
Toasts get the same surface as the outline button: a vertical gradient, a top highlight, a soft drop shadow and squircle corners. Each status icon sits in a small chip tinted with its colour.
"use client";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Toaster } from "@/components/ui/sonner";
export default function SonnerTypesExample() {
return (
<div className="flex min-h-80 w-full flex-wrap content-start items-center justify-center gap-3 p-6">
<Button onClick={() => toast("Workspace synced")} variant="outline">
Default
</Button>
<Button
onClick={() => toast.success("Deployment complete")}
variant="outline"
>
Success
</Button>
<Button
onClick={() =>
toast.info("New scan scheduled", {
description: "Runs every Monday at 09:00.",
})
}
variant="outline"
>
Info
</Button>
<Button
onClick={() =>
toast.warning("Seat limit almost reached", {
description: "9 of 10 seats are in use.",
})
}
variant="outline"
>
Warning
</Button>
<Button
onClick={() => toast.error("Something went wrong")}
variant="outline"
>
Error
</Button>
<Button
onClick={() => {
const id = toast.loading("Publishing changelog...");
setTimeout(() => toast.success("Changelog published", { id }), 2000);
}}
variant="outline"
>
Loading
</Button>
<Toaster position="bottom-center" />
</div>
);
}
Installation
$ bunx --bun shadcn@latest add @notra/notra-sonnerThis replaces components/ui/sonner.tsx and pulls in Button for the action buttons. Sonner still does the work underneath, so every toast() call keeps working. Mount the toaster once in your root layout:
import { Toaster } from "@/components/ui/sonner";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
{children}
<Toaster position="bottom-right" />
</body>
</html>
);
}
Status colours
Success, warning and info read your --success, --warning and --info tokens. If your theme doesn’t define them, the toaster falls back to Notra’s colours. Errors use --destructive.
Actions
"use client";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Toaster } from "@/components/ui/sonner";
export default function SonnerActionExample() {
return (
<div className="flex min-h-64 w-full flex-wrap content-start items-center justify-center gap-3 p-6">
<Button
onClick={() =>
toast("Post moved to trash", {
action: {
label: "Undo",
onClick: () => toast.success("Post restored"),
},
})
}
variant="outline"
>
With action
</Button>
<Button
onClick={() =>
toast.error("Couldn't change role", {
cancel: { label: "Dismiss", onClick: () => undefined },
description: "Your plan includes 3 team members.",
})
}
variant="outline"
>
With cancel
</Button>
<Toaster position="bottom-center" />
</div>
);
}
Action buttons render with Button at size xs, and cancel buttons use the ghost variant. The CLI installs Button along with the toaster, so they get the Depth look too.
toast("Post moved to trash", {
action: {
label: "Undo",
onClick: () => restorePost(),
},
});