- Accordion
- Alert
- Alert Dialog
- Autocomplete
- Avatar
- Badge
- Breadcrumb
- Button
- Card
- Checkbox
- Checkbox Group
- Collapsible
- Combobox
- Dialog
- EmptyNew
- Field
- Fieldset
- Form
- Frame
- Group
- Input
- Input GroupNew
- KbdNew
- Label
- Menu
- Meter
- Number Field
- Pagination
- Popover
- Preview Card
- Progress
- Radio Group
- Scroll Area
- Select
- Separator
- Sheet
- SkeletonNew
- Slider
- SpinnerNew
- Switch
- Table
- Tabs
- Textarea
- Toast
- Toggle
- Toggle Group
- Toolbar
- Tooltip
Toast
Generates toast notifications.
"use client";
import { Button } from "@/components/ui/button";
import { toastManager } from "@/components/ui/toast";
export default function Particle() {
return (
<Button
onClick={() => {
toastManager.add({
description: "Monday, January 3rd at 6:00pm",
title: "Event has been created",
});
}}
variant="outline"
>
Default Toast
</Button>
);
}
Installation
pnpm dlx shadcn@latest add @coss/toast
Add the ToastProvider and AnchoredToastProvider to your app.
import { AnchoredToastProvider, ToastProvider } from "@/components/ui/toast"
export default function RootLayout({ children }) {
return (
<html lang="en">
<head />
<body>
<ToastProvider>
<AnchoredToastProvider>
<main>{children}</main>
</AnchoredToastProvider>
</ToastProvider>
</body>
</html>
)
}Usage
Stacked Toasts
import { toastManager } from "@/components/ui/toast"toastManager.add({
title: "Event has been created",
description: "Monday, January 3rd at 6:00pm",
})By default, toasts appear in the bottom-right corner. You can change this by setting the position prop on the ToastProvider.
Allowed values: top-left, top-center, top-right, bottom-left, bottom-center, bottom-right. For example:
<ToastProvider position="top-center">{children}</ToastProvider>Anchored Toasts
For toasts positioned relative to a specific element, use anchoredToastManager. The AnchoredToastProvider is typically added to your app layout (alongside ToastProvider), so you can use anchoredToastManager directly in your components:
anchoredToastManager.add({
title: "Copied!",
positionerProps: {
anchor: buttonRef.current,
},
})You can also style anchored toasts like tooltips by passing data: { tooltipStyle: true }. When using tooltip style, only the title is displayed (description and other content are ignored):
anchoredToastManager.add({
title: "Copied!",
positionerProps: {
anchor: buttonRef.current,
},
data: {
tooltipStyle: true,
},
})Examples
With Status
"use client";
import { Button } from "@/components/ui/button";
import { toastManager } from "@/components/ui/toast";
export default function Particle() {
return (
<div className="flex flex-wrap gap-2">
<Button
onClick={() => {
toastManager.add({
description: "Your changes have been saved.",
title: "Success!",
type: "success",
});
}}
variant="outline"
>
Success Toast
</Button>
<Button
onClick={() => {
toastManager.add({
description: "There was a problem with your request.",
title: "Uh oh! Something went wrong.",
type: "error",
});
}}
variant="outline"
>
Error Toast
</Button>
<Button
onClick={() => {
toastManager.add({
description: "You can add components to your app using the cli.",
title: "Heads up!",
type: "info",
});
}}
variant="outline"
>
Info Toast
</Button>
<Button
onClick={() => {
toastManager.add({
description: "Your session is about to expire.",
title: "Warning!",
type: "warning",
});
}}
variant="outline"
>
Warning Toast
</Button>
</div>
);
}
Loading
"use client";
import { Button } from "@/components/ui/button";
import { toastManager } from "@/components/ui/toast";
export default function Particle() {
return (
<Button
onClick={() => {
toastManager.add({
description: "Please wait while we process your request.",
title: "Loading…",
type: "loading",
});
}}
variant="outline"
>
Loading Toast
</Button>
);
}
With Action
"use client";
import { Button } from "@/components/ui/button";
import { toastManager } from "@/components/ui/toast";
export default function Particle() {
return (
<Button
onClick={() => {
const id = toastManager.add({
actionProps: {
children: "Undo",
onClick: () => {
toastManager.close(id);
toastManager.add({
description: "The action has been reverted.",
title: "Action undone",
type: "info",
});
},
},
description: "You can undo this action.",
timeout: 1000000,
title: "Action performed",
type: "success",
});
}}
variant="outline"
>
Perform Action
</Button>
);
}
Promise
"use client";
import { Button } from "@/components/ui/button";
import { toastManager } from "@/components/ui/toast";
export default function Particle() {
return (
<Button
onClick={() => {
toastManager.promise(
new Promise<string>((resolve, reject) => {
const shouldSucceed = Math.random() > 0.3;
setTimeout(() => {
if (shouldSucceed) {
resolve("Data loaded successfully");
} else {
reject(new Error("Failed to load data"));
}
}, 2000);
}),
{
error: () => ({
description: "Please try again.",
title: "Something went wrong",
}),
loading: {
description: "The promise is loading.",
title: "Loading…",
},
success: (data: string) => ({
description: `Success: ${data}`,
title: "This is a success toast!",
}),
},
);
}}
variant="outline"
>
Run Promise
</Button>
);
}
With Varying Heights
"use client";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { toastManager } from "@/components/ui/toast";
const TEXTS = [
"Short message.",
"A bit longer message that spans two lines.",
"This is a longer description that intentionally takes more vertical space to demonstrate stacking with varying heights.",
"An even longer description that should span multiple lines so we can verify the clamped collapsed height and smooth expansion animation when hovering or focusing the viewport.",
];
export default function Particle() {
const [count, setCount] = React.useState(0);
function createToast() {
setCount((prev) => prev + 1);
const description = TEXTS[Math.floor(Math.random() * TEXTS.length)];
toastManager.add({
description,
timeout: 2000,
title: `Toast ${count + 1} created`,
});
}
return (
<Button onClick={createToast} variant="outline">
With Varying Heights
</Button>
);
}
Copy Button with Anchored Toast
"use client";
import { CheckIcon, CopyIcon } from "lucide-react";
import * as React from "react";
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard";
import { Button } from "@/components/ui/button";
import { anchoredToastManager } from "@/components/ui/toast";
import {
Tooltip,
TooltipPopup,
TooltipTrigger,
} from "@/components/ui/tooltip";
export default function Particle() {
const copyButtonRef = React.useRef<HTMLButtonElement>(null);
const toastTimeout = 2000;
const { copyToClipboard, isCopied } = useCopyToClipboard({
onCopy: () => {
if (copyButtonRef.current) {
anchoredToastManager.add({
data: {
tooltipStyle: true,
},
positionerProps: {
anchor: copyButtonRef.current,
},
timeout: toastTimeout,
title: "Copied!",
});
}
},
timeout: toastTimeout,
});
function handleCopy() {
const url = "https://coss.com";
copyToClipboard(url);
}
return (
<Tooltip>
<TooltipTrigger
render={
<Button
aria-label="Copy link"
disabled={isCopied}
onClick={handleCopy}
ref={copyButtonRef}
size="icon"
variant="outline"
/>
}
>
{isCopied ? (
<CheckIcon className="size-4" />
) : (
<CopyIcon className="size-4" />
)}
</TooltipTrigger>
<TooltipPopup>
<p>Copy to clipboard</p>
</TooltipPopup>
</Tooltip>
);
}
Submit Button with Error Toast
"use client";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import { anchoredToastManager } from "@/components/ui/toast";
export default function Particle() {
const submitRef = React.useRef<HTMLButtonElement>(null);
const [isSubmitting, setIsSubmitting] = React.useState(false);
const toastIdRef = React.useRef<string | null>(null);
function handleSubmit() {
if (!submitRef.current || isSubmitting) return;
if (toastIdRef.current) {
anchoredToastManager.close(toastIdRef.current);
toastIdRef.current = null;
}
setIsSubmitting(true);
new Promise<void>((_, reject) => {
setTimeout(() => {
setIsSubmitting(false);
reject(
new Error("The server is not responding. Please try again later."),
);
}, 2000);
}).catch((error: Error) => {
toastIdRef.current = anchoredToastManager.add({
description: error.message,
positionerProps: {
anchor: submitRef.current,
sideOffset: 4,
},
title: "Error submitting form",
type: "error",
});
});
}
return (
<Button
disabled={isSubmitting}
onClick={handleSubmit}
ref={submitRef}
variant="outline"
>
{isSubmitting ? (
<>
<Spinner />
Submitting…
</>
) : (
"Submit"
)}
</Button>
);
}
Comparing with Sonner / shadcn
The API is significantly different from shadcn/ui (Sonner). Please review both docs before migrating: Sonner Docs and shadcn/ui Sonner, and our Base UI toast docs referenced at the top of this page.
Comparison Examples
shadcn/ui (Sonner)
import { Toaster } from "@/components/ui/sonner"
export default function RootLayout({ children }) {
return (
<html lang="en">
<head />
<body>
<main>{children}</main>
<Toaster />
</body>
</html>
)
}toast("Event has been created", {
description: "Sunday, December 03, 2023 at 9:00 AM",
cancel: {
label: "Undo",
},
})coss ui (Base UI)
import { ToastProvider } from "@/components/ui/toast"
export default function RootLayout({ children }) {
return (
<html lang="en">
<head />
<body>
<ToastProvider>
<main>{children}</main>
</ToastProvider>
</body>
</html>
)
}onClick={() => {
const id = toastManager.add({
title: "Event has been created",
description: "Sunday, December 03, 2023 at 9:00 AM",
type: "success",
actionProps: {
children: "Undo",
onClick: () => toastManager.close(id),
},
})
}}