3

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

CreditsModifiedPublished

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.json

Usage

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&apos;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>
  )
}

API Reference

ToastProvider

AnchoredToastProvider

toast.add(options)

anchoredToast.add(options)