Toast
A temporary notification that stacks in a corner of the screen, or anchors to a specific element on the page. Built on Base UI's toast primitive, with swipe-to-dismiss and success/error/warning/ info/loading statuses.
Last updated August 9, 2026
Copied from shadcn/ui and coss ui.
- Adds a logical position="start"/"end" (on both ToastProvider's corner and the physical left/right CSS it drives) on top of coss ui's literal left/right/center, resolved from the ambient direction measured where the provider's children actually render — matching the pattern already used by Dialog/Drawer/Tooltip for portaled content
- ToastProvider and AnchoredToastProvider explicitly set dir on the portaled Viewport/Positioner from that same measurement, so swipe-to-dismiss direction and logical spacing resolve correctly even when the provider sits in an otherwise-LTR page
- Merges shadcn's per-slot exported pieces (ToastTitle/ToastDescription/ToastAction/ToastClose, and an explicit close button) with coss ui's position-aware viewport, stacking math, and AnchoredToastProvider
- Adds an "x" variant (compact rounded-full pill with an avatar/icon slot, in the style of X's native in-app notifications) on top of both sources' plain card style, selected per-toast via `data: { variant: "x" }`
- Aligns the leading icon/avatar to the title line (items-start) instead of centering it against the combined title+description block
- Adds a per-toast `data: { dir }` override so a toast's content direction can be forced regardless of the provider's ambient/measured direction — the provider's corner position still follows the ambient direction, only the toast's own text/logical spacing is overridden
- Dropped coss ui's custom swipe-replay keyframes (bounce-on-update, shake-on-error) — this repo's registry has no mechanism yet for shipping component-scoped @keyframes, unlike coss ui's registry-item cssVars/css fields
Overview
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import {
ToastPosition,
ToastPrimitive,
ToastProvider,
} from "@/components/ui/toast"
const POSITIONS: { value: ToastPosition; label: string }[] = [
{ value: "top-start", label: "Top Start" },
{ value: "top-center", label: "Top Center" },
{ value: "top-end", label: "Top End" },
{ value: "bottom-start", label: "Bottom Start" },
{ value: "bottom-center", label: "Bottom Center" },
{ value: "bottom-end", label: "Bottom End" },
]
export function ToastDemoExample() {
const manager = React.useMemo(() => ToastPrimitive.createToastManager(), [])
const [position, setPosition] = React.useState<ToastPosition>("bottom-end")
return (
<ToastProvider toastManager={manager} position={position}>
<div className="grid w-full max-w-xs grid-cols-3 gap-2">
{POSITIONS.map((item) => (
<Button
key={item.value}
variant="outline"
size="sm"
className="text-xs"
onClick={() => {
setPosition(item.value)
manager.add({
title: "Event created",
description: "Sunday, December 3 at 9:00 AM",
actionProps: {
children: "Undo",
onClick: () => manager.add({ title: "Undone" }),
},
})
}}
>
{item.label}
</Button>
))}
</div>
</ToastProvider>
)
}Installation
$ npx shadcn@latest add https://ui.persian-labs.ir/r/toast.jsonUsage
Call toast.add() from anywhere inside a ToastProvider — no hook needed, since the manager is a plain module-level instance.
import { Button } from "@/components/ui/button"
import { toast, ToastProvider } from "@/components/ui/toast"
export function Example() {
return (
<ToastProvider>
<Button
variant="outline"
onClick={() =>
toast.add({
title: "Event created",
description: "Sunday, December 3 at 9:00 AM",
})
}
>
Show toast
</Button>
</ToastProvider>
)
}Statuses
Pass type to pick the leading icon and its color: success, error, warning, info, or loading. Update a loading toast in place with toast.update(id, options).
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import {
ToastPosition,
ToastPrimitive,
ToastProvider,
} from "@/components/ui/toast"
import { cn } from "@/lib/utils"
const POSITIONS: { value: ToastPosition; label: string }[] = [
{ value: "top-start", label: "Top Start" },
{ value: "top-center", label: "Top Center" },
{ value: "top-end", label: "Top End" },
{ value: "bottom-start", label: "Bottom Start" },
{ value: "bottom-center", label: "Bottom Center" },
{ value: "bottom-end", label: "Bottom End" },
]
export function ToastStatusesExample() {
const manager = React.useMemo(() => ToastPrimitive.createToastManager(), [])
const [position, setPosition] = React.useState<ToastPosition>("bottom-end")
return (
<ToastProvider toastManager={manager} position={position}>
<div className="flex w-full max-w-sm flex-col items-center gap-4">
<div className="grid w-full max-w-xs grid-cols-3 gap-2">
{POSITIONS.map((item) => (
<Button
key={item.value}
variant={position === item.value ? "secondary" : "outline"}
size="sm"
className={cn(
"text-xs",
position === item.value && "pointer-events-none"
)}
onClick={() => setPosition(item.value)}
>
{item.label}
</Button>
))}
</div>
<div className="flex flex-wrap justify-center gap-2">
<Button
variant="outline"
onClick={() =>
manager.add({
type: "success",
title: "Payment successful",
description: "Your invoice has been paid.",
})
}
>
Success
</Button>
<Button
variant="outline"
onClick={() =>
manager.add({
type: "error",
title: "Payment failed",
description: "Your card was declined.",
})
}
>
Error
</Button>
<Button
variant="outline"
onClick={() =>
manager.add({
type: "warning",
title: "Storage almost full",
description: "You're using 92% of your quota.",
})
}
>
Warning
</Button>
<Button
variant="outline"
onClick={() =>
manager.add({
type: "info",
title: "New version available",
description: "Refresh the page to update.",
})
}
>
Info
</Button>
<Button
variant="outline"
onClick={() => {
const id = manager.add({
type: "loading",
title: "Uploading file…",
timeout: 0,
})
setTimeout(() => {
manager.update(id, {
type: "success",
title: "File uploaded",
timeout: 4000,
})
}, 1800)
}}
>
Loading
</Button>
</div>
</div>
</ToastProvider>
)
}X style
Set data: { variant: "x" } for a compact, rounded-full pill in the style of X's native in-app notifications, with an avatar or icon slot.
"use client"
import { HeartIcon } from "lucide-react"
import * as React from "react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
ToastPosition,
ToastPrimitive,
ToastProvider,
} from "@/components/ui/toast"
const POSITIONS: { value: ToastPosition; label: string }[] = [
{ value: "top-start", label: "Top Start" },
{ value: "top-center", label: "Top Center" },
{ value: "top-end", label: "Top End" },
{ value: "bottom-start", label: "Bottom Start" },
{ value: "bottom-center", label: "Bottom Center" },
{ value: "bottom-end", label: "Bottom End" },
]
export function ToastXExample() {
const manager = React.useMemo(() => ToastPrimitive.createToastManager(), [])
const [position, setPosition] = React.useState<ToastPosition>("top-center")
return (
<ToastProvider toastManager={manager} position={position}>
<div className="grid w-full max-w-xs grid-cols-3 gap-2">
{POSITIONS.map((item) => (
<Button
key={item.value}
variant="outline"
size="sm"
className="text-xs"
onClick={() => {
setPosition(item.value)
manager.add({
title: "Ali liked your post",
description: "just now",
data: {
variant: "x",
avatar: (
<Avatar className="size-full">
<AvatarImage src="https://i.pravatar.cc/64?img=12" />
<AvatarFallback>
<HeartIcon className="size-3.5 fill-current text-red-500" />
</AvatarFallback>
</Avatar>
),
},
})
}}
>
{item.label}
</Button>
))}
</div>
</ToastProvider>
)
}Anchored toast
Wrap in AnchoredToastProvider and call anchoredToast.add({ positionerProps: { anchor } }) to position a toast against a specific element — a tooltip-like confirmation next to the button that triggered it — instead of stacking it in a screen corner. The smallest way to see it in action is Copy Button, a small building block that anchors its own copy confirmation this way — the Dialog and Drawer examples below use it too.
import { CopyButton } from "@/components/ui/copy-button"
export function Example() {
return <CopyButton text="https://persian-labs.ir" />
}Standalone
"use client"
import { CopyButton } from "@/components/ui/copy-button"
export function ToastAnchoredExample() {
return <CopyButton text="https://persian-labs.ir" label="Copy link" />
}From a Dialog
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import { CopyButton } from "@/components/ui/copy-button"
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
AnchoredToastProvider,
ToastPrimitive,
} from "@/components/ui/toast"
export function ToastAnchoredDialogExample() {
const manager = React.useMemo(() => ToastPrimitive.createToastManager(), [])
return (
<AnchoredToastProvider toastManager={manager}>
<Dialog>
<DialogTrigger render={<Button variant="outline">Edit profile</Button>} />
<DialogPopup>
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're
done.
</DialogDescription>
</DialogHeader>
<DialogPanel>
<div className="flex items-center gap-2 rounded-lg border bg-muted/50 px-3 py-2 text-sm">
<span className="min-w-0 flex-1 truncate text-muted-foreground">
https://persian-labs.ir/u/ali
</span>
<CopyButton
text="https://persian-labs.ir/u/ali"
variant="ghost"
size="icon-sm"
/>
</div>
</DialogPanel>
<DialogFooter>
<DialogClose render={<Button variant="outline">Cancel</Button>} />
<SaveButton manager={manager} />
</DialogFooter>
</DialogPopup>
</Dialog>
</AnchoredToastProvider>
)
}
function SaveButton({
manager,
}: {
manager: ReturnType<typeof ToastPrimitive.createToastManager>
}) {
const ref = React.useRef<HTMLButtonElement>(null)
return (
<Button
ref={ref}
type="submit"
onClick={() =>
manager.add({
type: "success",
title: "Profile saved",
positionerProps: {
anchor: ref.current,
side: "top",
},
})
}
>
Save changes
</Button>
)
}From a Drawer
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import { CopyButton } from "@/components/ui/copy-button"
import {
Drawer,
DrawerClose,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerPanel,
DrawerPopup,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
import {
AnchoredToastProvider,
ToastPrimitive,
} from "@/components/ui/toast"
export function ToastAnchoredDrawerExample() {
const manager = React.useMemo(() => ToastPrimitive.createToastManager(), [])
return (
<AnchoredToastProvider toastManager={manager}>
<Drawer>
<DrawerTrigger render={<Button variant="outline">Open drawer</Button>} />
<DrawerPopup showBar>
<DrawerHeader>
<DrawerTitle>Notification settings</DrawerTitle>
<DrawerDescription>
Choose what you want to be notified about.
</DrawerDescription>
</DrawerHeader>
<DrawerPanel>
<div className="flex items-center gap-2 rounded-lg border bg-muted/50 px-3 py-2 text-sm">
<span className="min-w-0 flex-1 truncate text-muted-foreground">
webhook_2f8a1c9e4b7d
</span>
<CopyButton
text="webhook_2f8a1c9e4b7d"
variant="ghost"
size="icon-sm"
/>
</div>
</DrawerPanel>
<DrawerFooter>
<SaveButton manager={manager} />
<DrawerClose render={<Button variant="outline">Cancel</Button>} />
</DrawerFooter>
</DrawerPopup>
</Drawer>
</AnchoredToastProvider>
)
}
function SaveButton({
manager,
}: {
manager: ReturnType<typeof ToastPrimitive.createToastManager>
}) {
const ref = React.useRef<HTMLButtonElement>(null)
return (
<Button
ref={ref}
type="submit"
onClick={() =>
manager.add({
type: "success",
title: "Preferences saved",
positionerProps: {
anchor: ref.current,
side: "top",
},
})
}
>
Save
</Button>
)
}RTL
By default every position — including the centered ones — mirrors automatically: start lands on the right in RTL and the left in LTR (and vice versa for end), matching the ambient dir the provider measures from its parent. To force a toast's content direction regardless of the ambient page direction — e.g. Persian content on an otherwise-LTR page — pass data: { dir: "rtl" } per call, as this example does rather than relying on the page direction toggle.
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import {
ToastPosition,
ToastPrimitive,
ToastProvider,
} from "@/components/ui/toast"
const POSITIONS: { value: ToastPosition; label: string }[] = [
{ value: "top-start", label: "بالا راست" },
{ value: "top-center", label: "بالا وسط" },
{ value: "top-end", label: "بالا چپ" },
{ value: "bottom-start", label: "پایین راست" },
{ value: "bottom-center", label: "پایین وسط" },
{ value: "bottom-end", label: "پایین چپ" },
]
export function ToastRtlExample() {
const manager = React.useMemo(() => ToastPrimitive.createToastManager(), [])
const [position, setPosition] = React.useState<ToastPosition>("bottom-end")
return (
<ToastProvider toastManager={manager} position={position}>
<div className="grid w-full max-w-xs grid-cols-3 gap-2">
{POSITIONS.map((item) => (
<Button
key={item.value}
variant="outline"
size="sm"
className="text-xs"
onClick={() => {
setPosition(item.value)
// Force this toast's content to render RTL, overriding the
// ambient direction the provider would otherwise measure —
// useful when a toast's content is Persian even though the
// page around it is LTR.
manager.add({
type: "success",
title: "رویداد ایجاد شد",
description: "یکشنبه، ۳ آذر، ساعت ۹:۰۰ صبح",
data: { dir: "rtl" },
actionProps: {
children: "واگرد",
onClick: () =>
manager.add({ title: "واگرد شد", data: { dir: "rtl" } }),
},
})
}}
>
{item.label}
</Button>
))}
</div>
</ToastProvider>
)
}