Skip to content
goodtabs UI
BrowseBrowse components

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

PropTypeDefaultDescription
positionToasterProps["position"]"bottom-right"Where toasts stack. Any Sonner position.
...propsToasterPropsEvery 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