---
import { Button } from "@/components/ui/button"
import { Toaster } from "@/components/ui/toast"
---
<div data-toast-demo>
<Button variant="outline" data-show-toast>
Show Toast
</Button>
<Toaster portal />
</div>
<script>
const initialize = () => {
for (const root of document.querySelectorAll("[data-toast-demo]")) {
if (root.hasAttribute("data-bound")) continue
root.setAttribute("data-bound", "")
const toaster = root.querySelector('[data-slot="toast"]')
root.querySelector("[data-show-toast]")?.addEventListener("click", () => {
// The action closes its toast, like toast.close(id) in the React example.
toaster?.dispatchEvent(
new CustomEvent("toast:show", {
detail: {
title: "Event created",
description: "Sunday, December 3 at 9:00 AM",
action: { label: "Undo", value: "undo" },
},
})
)
})
}
}
initialize()
document.addEventListener("astro:page-load", initialize)
</script>Installation
npx shadcn@latest add @fulldev/toast
Usage
Place one Toaster in the page shell or layout. Give it an ID when commands
need a stable target.
---
import { Toaster } from "@/components/ui/toast"
---
<Toaster id="notifications" portal />
Show a notification by dispatching a toast:show event on that root.
const toaster = document.getElementById("notifications")
toaster?.dispatchEvent(
new CustomEvent("toast:show", {
detail: {
title: "Changes saved",
description: "All updates are synced.",
type: "success",
},
})
)
When Astro’s ClientRouter can replace the page containing your trigger,
reconnect page-specific event handlers on astro:page-load. Keep a toaster in
the shared layout when the notification API should remain available across
route changes. Active notifications are reset during a route swap; persist any
notification data in your own application state when it must survive.
Options
Toaster accepts these runtime options as props:
limit: maximum visible notifications, default3duration: automatic dismissal delay in milliseconds, default5000; use0for persistent notificationsposition:top-left,top-center,top-right,bottom-left,bottom-center, orbottom-rightpauseOnHoverandpauseOnFocus: pause active timers during interactionportal: move the viewport todocument.body
Toast data accepts id, title, description, type, duration, action,
dismissible, closeButtonAriaLabel, and testId. title is required. Types
are default, success, error, warning, info, and loading.
Types
Set type to render a status icon: success, info, warning, error, or loading.
---
import { Button } from "@/components/ui/button"
import { Toaster } from "@/components/ui/toast"
const toasts = [
{ label: "Default", description: "Event has been created." },
{
label: "Success",
type: "success",
description: "Event has been created.",
},
{
label: "Info",
type: "info",
description: "Arrive 10 minutes before the event.",
},
{
label: "Warning",
type: "warning",
description: "The event cannot start before 8:00 AM.",
},
{
label: "Error",
type: "error",
description: "The event could not be created.",
},
]
---
<div class="flex flex-wrap gap-2" data-toast-types>
{toasts.map((toast) => (
<Button
variant="outline"
data-toast-type={toast.type}
data-toast-description={toast.description}
>
{toast.label}
</Button>
))}
<Toaster portal />
</div>
<script>
const initialize = () => {
for (const root of document.querySelectorAll("[data-toast-types]")) {
if (root.hasAttribute("data-bound")) continue
root.setAttribute("data-bound", "")
const toaster = root.querySelector('[data-slot="toast"]')
for (const button of root.querySelectorAll<HTMLElement>(
"[data-toast-description]"
)) {
button.addEventListener("click", () => {
const { toastType, toastDescription } = button.dataset
// A toast needs a title, so the text shows as the title.
toaster?.dispatchEvent(
new CustomEvent("toast:show", {
detail: { title: toastDescription, type: toastType },
})
)
})
}
}
}
initialize()
document.addEventListener("astro:page-load", initialize)
</script>Action
Pass action with a label and value. The action closes its toast; listen for toast:action to handle it.
Promise
Show a loading toast with an id and duration: 0, then patch it with toast:update when the task ends.
---
import { Button } from "@/components/ui/button"
import { Toaster } from "@/components/ui/toast"
---
<div data-toast-promise>
<Button variant="outline" data-create-event>
Create Event
</Button>
<Toaster portal />
</div>
<script>
const initialize = () => {
for (const root of document.querySelectorAll("[data-toast-promise]")) {
if (root.hasAttribute("data-bound")) continue
root.setAttribute("data-bound", "")
const toaster = root.querySelector('[data-slot="toast"]')
const send = (name: string, detail: object) =>
toaster?.dispatchEvent(new CustomEvent(name, { detail }))
root
.querySelector("[data-create-event]")
?.addEventListener("click", async () => {
// One toast moves from loading to success or error.
const id = `event-${Date.now()}`
send("toast:show", {
id,
title: "Creating event…",
type: "loading",
duration: 0,
})
try {
const data = await new Promise<{ name: string }>((resolve) => {
window.setTimeout(() => resolve({ name: "Event" }), 2000)
})
send("toast:update", {
id,
title: `${data.name} created.`,
type: "success",
duration: 5000,
})
} catch {
send("toast:update", {
id,
title: "Could not create event.",
type: "error",
duration: 5000,
})
}
})
}
}
initialize()
document.addEventListener("astro:page-load", initialize)
</script>Commands
The root listens for framework-neutral DOM events.
// Show and keep a stable ID for later updates.
toaster.dispatchEvent(
new CustomEvent("toast:show", {
detail: { id: "upload", title: "Uploading", type: "loading", duration: 0 },
})
)
// Patch an active toast.
toaster.dispatchEvent(
new CustomEvent("toast:update", {
detail: { id: "upload", title: "Upload complete", type: "success" },
})
)
// Dismiss one toast or clear all active toasts.
toaster.dispatchEvent(
new CustomEvent("toast:dismiss", { detail: { id: "upload" } })
)
toaster.dispatchEvent(new CustomEvent("toast:clear"))
The root emits toast:change with { id, action: "show" | "dismiss" } and
toast:action with { id, value }. Listen for toast:action to handle action
buttons.
toaster.addEventListener("toast:action", (event) => {
if (event.detail.value === "undo-event") {
// Undo the operation here.
}
})
For controller access, import createToast from @data-slot/toast. Its
show, update, promise, dismiss, and dismissAll methods use the same
data model. The Astro Toast component already initializes roots, so create a
controller directly only when you own initialization yourself.
Custom template
Toaster includes the standard template and viewport. Use the lower-level
parts when you need different markup or classes.
---
import {
Toast,
ToastAction,
ToastClose,
ToastContent,
ToastDescription,
ToastIcon,
ToastItem,
ToastTemplate,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
---
<Toast id="notifications">
<ToastTemplate>
<ToastItem>
<ToastContent>
<ToastIcon />
<div class="min-w-0 flex-1">
<ToastTitle />
<ToastDescription />
</div>
<ToastAction />
<ToastClose />
</ToastContent>
</ToastItem>
</ToastTemplate>
<ToastViewport />
</Toast>
API Reference
Source: src/components/ui/toast
See the data-slot docs for controller methods, promise notifications, timers, stacking, swipe dismissal, and events.