ComponentsCSS
Toast
Sonner's stacking, swiping and promise toasts, dressed in the theme.
Position
Installation
npx shadcn@latest add https://ui-collections.vercel.app/r/tide/toast.jsonAlso installs the theme, cn(). Packages: clsx, tailwind-merge, sonner.
Usage
toast-demo.tsx
"use client";
import { useState } from "react";
import { cn } from "@/registry/kit/lib/utils";
import { Button } from "@/registry/kit/ui/button";
import { toast, Toaster, type ToastPosition } from "@/registry/kit/ui/toast";
const deploy = () => new Promise((resolve) => setTimeout(resolve, 1800));
const positions: ToastPosition[][] = [
["top-left", "top-center", "top-right"],
["bottom-left", "bottom-center", "bottom-right"],
];
export default function ToastDemo() {
const [position, setPosition] = useState<ToastPosition>("bottom-center");
return (
<div className="grid justify-items-center gap-6">
<Toaster position={position} />
<div className="flex flex-wrap justify-center gap-3">
<Button onClick={() => toast("Link copied", { description: "Anyone with the link can view this preview." })}>Copy preview link</Button>
<Button onClick={() => toast.success("Environment variable saved")}>Save variable</Button>
<Button
onClick={() =>
toast("Deployment deleted", {
action: { label: "Undo", onClick: () => toast.success("Deployment restored") },
})
}
>
Delete deployment
</Button>
<Button
variant="solid"
onClick={() => toast.promise(deploy, { loading: "Deploying…", success: "Live on production", error: "Deploy failed" })}
>
Deploy to production
</Button>
</div>
<div className="grid gap-2 text-center">
<p id="toast-position" className="text-xs text-muted">
Position
</p>
<div role="radiogroup" aria-labelledby="toast-position" className="surface grid grid-cols-3 gap-1 rounded-field p-1">
{positions.flat().map((p) => (
<button
key={p}
type="button"
role="radio"
aria-checked={position === p}
onClick={() => {
setPosition(p);
toast(`Toasts now appear ${p.replace("-", " ")}`);
}}
className={cn(
"h-8 cursor-pointer rounded-[max(0.125rem,calc(var(--r-field)-0.25rem))] px-3 text-xs capitalize transition-colors focus-visible:outline-2 focus-visible:outline-accent",
position === p ? "bg-fg/10 font-medium text-fg" : "text-muted hover:text-fg",
)}
>
{p.replace("-", " ")}
</button>
))}
</div>
</div>
</div>
);
}API
<Toaster>Sonner propsMount once near the root.
position= "bottom-center"
"top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right"Where toasts stack. Bottom positions lift above an open selection bar.
toast()functiontoast, toast.success, toast.error, toast.promise…
Source
components/ui/toast.tsx
"use client";
import { useSyncExternalStore, type ComponentProps } from "react";
import { createPortal } from "react-dom";
import { Toaster as Sonner, toast } from "sonner";
import { cn } from "@/registry/kit/lib/utils";
import { Spinner } from "@/registry/kit/ui/spinner";
export { toast };
export type ToastPosition = "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right";
// Never wrap or shrink: the message gives way instead.
const button = "shrink-0 cursor-pointer whitespace-nowrap rounded-control px-3 py-1 text-xs font-medium transition-colors";
/**
* Sonner's stacking and gestures with surface toasts. Mount once near the root,
* then call `toast("…")`, `toast.success(…)`, `toast.promise(…)` anywhere.
*/
const subscribe = () => () => {};
export function Toaster({ toastOptions, className, ...props }: ComponentProps<typeof Sonner>) {
// Rendered into <body>: mounted inside a demo frame, sticky header or any
// `isolate` container, top toasts would otherwise slide under the page's nav.
const client = useSyncExternalStore(subscribe, () => true, () => false);
if (!client) return null;
return createPortal(
<Sonner
position="bottom-center"
// Our spinner, in the same slot and size as the other toast icons.
icons={{ loading: <Spinner size="sm" label={null} />, ...props.icons }}
// While a SelectionBar shows, toasts rise above it instead of covering it.
className={cn("[html[data-selection-bar]_&[data-y-position=bottom]]:bottom-(--selection-bar-space)!", className)}
gap={10}
toastOptions={{
unstyled: true,
...toastOptions,
classNames: {
toast: cn(
"surface-raised flex w-(--width) items-center gap-3 rounded-card px-4 py-3.5 text-sm text-fg",
),
content: "min-w-0 flex-1",
title: "font-medium",
description: "mt-0.5 text-[13px] text-muted",
icon: "shrink-0 text-accent [&_svg]:size-4",
// Sonner centres its loader absolutely inside the toast; keep it in the icon slot.
loader: "static! transform-none! grid place-items-center",
success: "[&_[data-icon]]:text-success",
error: "[&_[data-icon]]:text-danger",
warning: "[&_[data-icon]]:text-warning",
actionButton: cn(button, "ml-auto bg-fg text-bg hover:bg-fg/90"),
cancelButton: cn(button, "bg-fg/10 text-fg hover:bg-fg/15"),
...toastOptions?.classNames,
},
}}
{...props}
/>,
document.body,
);
}