Toaster
Sonner set up for the goodtabs look: bottom right, monochrome, with lucide icons.
npx shadcn@latest add https://ui.goodtabs.co/r/toaster.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/toaster
Success, error, a promise that goes from loading to done, and a toast with an undo action.
Info and warning
Any Sonner position works. Descriptions sit in muted text under the title.
Usage
import { Toaster, toast } from "@/components/ui/toaster"
// once, in your root layout
<Toaster />
// anywhere
toast.success("Saved")
toast.promise(save(), { loading: "Saving", success: "Saved", error: "Could not save" })Props
| Prop | Type | Default | Description |
|---|---|---|---|
| position | ToasterProps["position"] | "bottom-right" | Where toasts stack. Any Sonner position. |
| ...props | ToasterProps | Every other Sonner Toaster prop passes through and overrides the defaults. |
Source
toaster/toaster.tsx
"use client";
import * as React from "react";
import { CircleCheckIcon, InfoIcon, Loader2Icon, OctagonXIcon, TriangleAlertIcon } from "lucide-react";
import { Toaster as Sonner, toast, type ToasterProps } from "sonner";
/** Watches the html class list, so the toasts follow the page theme without a theme library. */
function useHtmlTheme(): "light" | "dark" {
const subscribe = React.useCallback((onChange: () => void) => {
const observer = new MutationObserver(onChange);
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
return () => observer.disconnect();
}, []);
const read = () => (document.documentElement.classList.contains("dark") ? "dark" : "light");
return React.useSyncExternalStore(subscribe, read, () => "light");
}
/**
* Sonner, set up for the goodtabs look: bottom right, monochrome, lucide icons, tokens only.
* Mount it once near the root, then call toast() from anywhere.
*/
export function Toaster({ position = "bottom-right", ...props }: ToasterProps) {
const theme = useHtmlTheme();
return (
<Sonner
theme={theme}
position={position}
closeButton={false}
richColors={false}
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin motion-reduce:animate-none" />,
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
"--font-family": "var(--font-sans)",
} as React.CSSProperties
}
toastOptions={{
classNames: {
toast: "!shadow-lg !border !bg-popover !text-popover-foreground !text-sm",
title: "!font-medium",
description: "!text-muted-foreground",
icon: "!text-foreground",
actionButton:
"!h-7 !rounded-md !border !border-input !bg-background !px-2.5 !text-xs !font-medium !text-foreground !shadow-xs hover:!bg-muted",
cancelButton:
"!h-7 !rounded-md !bg-transparent !px-2.5 !text-xs !font-medium !text-muted-foreground hover:!bg-muted hover:!text-foreground",
},
}}
{...props}
/>
);
}
export { toast };
For AI agents
Hand your agent this page, the markdown at /components/toaster/prompt.md, or the Copy AI prompt button above.
When to use
- Confirming that a quick action worked: saved, copied, sent
- Reporting a background task that finished or failed
- Offering undo right after a reversible action
When not to use
- Errors the person must deal with before moving on: use an inline message or a dialog
- Anything that needs to stay on screen: toasts go away on their own
Agent notes
- Mount Toaster once, in the root layout, then import toast from the same file anywhere
- It reads the theme from the dark class on the html element, so it works with next-themes or any class based toggle without importing either
- richColors is off on purpose: every toast type is monochrome and the icon carries the meaning
- Pass position or any other Sonner prop to override the defaults for one mount