import { Button } from "@registry/components/ui/button";
import { toastManager } from "@registry/components/ui/toast";
export function ToastExample() {
return (
<Button
variant="outline"
onClick={() => {
toastManager.add({
title: "Changes saved",
description: "Your profile settings have been updated.",
type: "success",
});
}}
>
Show toast
</Button>
);
} Examples
Toast uses a shared toastManager, so render ToastProvider once near your app shell, then trigger notifications from anywhere.
Types and actions
Use the type field to select a built-in icon, and pass actionProps to render an inline action button.
import { Button } from "@registry/components/ui/button";
import { toastManager } from "@registry/components/ui/toast";
export function ToastVariantsExample() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button
variant="outline"
onClick={() => {
toastManager.add({
title: "Update available",
description: "Version 1.3.0 is ready to install.",
type: "info",
actionProps: {
children: "Review",
onClick: () => {
toastManager.add({
title: "Release notes opened",
description: "You can review the changelog before upgrading.",
type: "success",
});
},
},
});
}}
>
Info
</Button>
<Button
variant="outline"
onClick={() => {
toastManager.add({
title: "Deployment failed",
description: "Reconnect your repository and try again.",
type: "error",
actionProps: {
children: "Retry",
onClick: () => {
toastManager.add({
title: "Retry started",
description: "We’re attempting the deployment again.",
type: "loading",
});
},
},
});
}}
>
Error
</Button>
<Button
variant="outline"
onClick={() => {
toastManager.add({
title: "Finishing sync",
description: "This toast stays visible until you dismiss it.",
timeout: 0,
type: "warning",
});
}}
>
Persistent
</Button>
</div>
);
} Position
Pass the position prop to ToastProvider to place the stack at any screen edge or center.
import { useState } from "react";
import { Button } from "@registry/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@registry/components/ui/select";
import { ToastProvider, ToastPosition } from "@registry/components/ui/toast";
export function ToastPositionExample() {
const [position, setPosition] = useState<ToastPosition>("bottom-right");
return (
<ToastProvider position={position} toastManager={positionedToastManager}>
<div className="flex flex-col items-start gap-3">
<Select
items={POSITION_ITEMS}
value={position}
onValueChange={(value) => {
if (value) {
setPosition(value as ToastPosition);
}
}}
>
<SelectTrigger variant="outline" className="w-44">
<SelectValue placeholder="Select a position" />
</SelectTrigger>
<SelectContent>
{POSITION_ITEMS.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="outline"
onClick={() => {
positionedToastManager.add({
title: "Position updated",
description: `This toast is rendered at ${position}.`,
type: "info",
});
}}
>
Show positioned toast
</Button>
</div>
</ToastProvider>
);
} Promise states
Use toastManager.promise(...) to keep loading, success, and error feedback tied to the same async flow.
import { Button } from "@registry/components/ui/button";
import { toastManager } from "@registry/components/ui/toast";
export function ToastPromiseExample() {
return (
<Button
variant="outline"
onClick={() => {
void toastManager.promise(sleep(1500), {
loading: {
title: "Uploading asset",
description: "Please wait while we process your file.",
type: "loading",
},
success: {
title: "Upload complete",
description: "The asset is ready to use in your library.",
type: "success",
},
error: {
title: "Upload failed",
description: "Try again after checking your network connection.",
type: "error",
},
});
}}
>
Run async toast
</Button>
);
} Usage
Mount the provider once:
import { ToastProvider } from "@registry/components/ui/toast";
export function AppShell() {
return (
<ToastProvider position="bottom-right">
<App />
</ToastProvider>
);
} Trigger a toast from anywhere you can import the manager:
import { toastManager } from "@registry/components/ui/toast";
toastManager.add({
title: "Changes saved",
description: "Your preferences are now up to date.",
type: "success",
}); Installation
Copy the source code below into your project:
import { Toast } from "@base-ui/react/toast";
import { buttonVariants } from "./button";
import { cn } from "@registry/lib/utils";
import { CheckCircleIcon, InfoIcon, SpinnerIcon, WarningIcon } from "@phosphor-icons/react";
const toastManager = Toast.createToastManager();
const TOAST_ICONS = {
error: WarningIcon,
info: InfoIcon,
loading: SpinnerIcon,
success: CheckCircleIcon,
warning: WarningIcon,
} as const;
type ToastPosition =
| "top-left"
| "top-center"
| "top-right"
| "bottom-left"
| "bottom-center"
| "bottom-right";
interface ToastProviderProps extends Toast.Provider.Props {
position?: ToastPosition;
}
function ToastProvider({
children,
position = "bottom-right",
...props
}: ToastProviderProps) {
const isTop = position.startsWith("top");
return (
<Toast.Provider toastManager={toastManager} {...props}>
{children}
<ToastHost position={position} isTop={isTop} />
</Toast.Provider>
);
}
function ToastHost({ position, isTop }: { position: ToastPosition; isTop: boolean }) {
const { toasts } = Toast.useToastManager();
return (
<Toast.Portal data-slot="toast-portal">
<Toast.Viewport
className={cn(
"fixed z-50 mx-auto flex w-[calc(100%-var(--toast-inset)*2)] max-w-90 [--toast-inset:--spacing(4)] sm:[--toast-inset:--spacing(8)]",
// Vertical positioning
"data-[position*=top]:top-(--toast-inset)",
"data-[position*=bottom]:bottom-(--toast-inset)",
// Horizontal positioning
"data-[position*=left]:left-(--toast-inset)",
"data-[position*=right]:right-(--toast-inset)",
"data-[position*=center]:left-1/2 data-[position*=center]:-translate-x-1/2",
)}
data-position={position}
data-slot="toast-viewport"
>
{toasts.map((toast) => {
const Icon = toast.type ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] : null;
return (
<Toast.Root
className={cn(
"absolute z-[calc(9999-var(--toast-index))] h-(--toast-calc-height) w-full rounded-[34px] bg-gray-950 bg-clip-padding px-3.5 py-3 text-gray-50 select-none [transition:transform_.5s_cubic-bezier(.22,1,.36,1),opacity_.5s,filter_.5s,height_.15s] before:pointer-events-none before:absolute before:inset-0 before:z-0 before:rounded-[inherit] before:bg-[rgb(255_255_255/calc(min(var(--toast-index),2)*0.045))] before:content-[''] data-expanded:before:bg-transparent dark:bg-gray-50 dark:text-gray-950 dark:before:bg-[rgb(0_0_0/calc(min(var(--toast-index),2)*0.04))]",
// Base positioning using data-position
"data-[position*=right]:right-0 data-[position*=right]:left-auto",
"data-[position*=left]:right-auto data-[position*=left]:left-0",
"data-[position*=center]:right-0 data-[position*=center]:left-0",
"data-[position*=top]:top-0 data-[position*=top]:bottom-auto data-[position*=top]:origin-top",
"data-[position*=bottom]:top-auto data-[position*=bottom]:bottom-0 data-[position*=bottom]:origin-bottom",
// Gap fill for hover
"after:absolute after:left-0 after:h-[calc(var(--toast-gap)+1px)] after:w-full",
"data-[position*=top]:after:top-full",
"data-[position*=bottom]:after:bottom-full",
// Define some variables
"[--toast-calc-height:var(--toast-frontmost-height,var(--toast-height))] [--toast-gap:--spacing(3)] [--toast-peek:--spacing(3)] [--toast-scale:calc(max(0,1-(var(--toast-index)*.1)))] [--toast-shrink:calc(1-var(--toast-scale))]",
// Define offset-y variable
"data-[position*=top]:[--toast-calc-offset-y:calc(var(--toast-offset-y)+var(--toast-index)*var(--toast-gap)+var(--toast-swipe-movement-y))]",
"data-[position*=bottom]:[--toast-calc-offset-y:calc(var(--toast-offset-y)*-1+var(--toast-index)*var(--toast-gap)*-1+var(--toast-swipe-movement-y))]",
// Default state transform
"data-[position*=top]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--toast-peek))+(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]",
"data-[position*=bottom]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--toast-peek))-(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]",
// Limited state
"data-limited:opacity-0 data-limited:filter-[blur(16px)]",
// Expanded state
"data-expanded:h-(--toast-height)",
"data-position:data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(var(--toast-calc-offset-y))]",
// Starting and ending animations
"data-[position*=top]:data-starting-style:transform-[translateY(calc(-100%-var(--toast-inset)))]",
"data-[position*=bottom]:data-starting-style:transform-[translateY(calc(100%+var(--toast-inset)))]",
"data-ending-style:opacity-0",
"data-ending-style:not-data-limited:not-data-swipe-direction:filter-[blur(16px)]",
// Ending animations (direction-aware)
"data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
"data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
"data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]",
"data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]",
// Ending animations (expanded)
"data-expanded:data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
"data-expanded:data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
"data-expanded:data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]",
"data-expanded:data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]",
)}
data-position={position}
key={toast.id}
swipeDirection={
position.includes("center")
? [isTop ? "up" : "down"]
: position.includes("left")
? ["left", isTop ? "up" : "down"]
: ["right", isTop ? "up" : "down"]
}
toast={toast}
>
<Toast.Content className="relative z-10 flex items-center justify-between gap-1.5 overflow-hidden text-sm transition-opacity duration-250 data-behind:pointer-events-none data-behind:opacity-0 data-expanded:pointer-events-auto data-expanded:opacity-100">
<div className="flex gap-2">
{Icon && (
<div className="flex items-center justify-center p-1 pr-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&>svg]:size-5">
<Icon className="in-data-[type=error]:text-red-500 in-data-[type=info]:text-gray-500 in-data-[type=loading]:animate-spin in-data-[type=loading]:opacity-72 in-data-[type=success]:text-green-500 in-data-[type=warning]:text-orange-500" />
</div>
)}
<div className="flex flex-col gap-0.5">
<Toast.Title className="font-medium" data-slot="toast-title" />
<Toast.Description
className="text-muted-foreground"
data-slot="toast-description"
/>
</div>
</div>
{toast.actionProps && (
<Toast.Action
className={cn(
buttonVariants({ size: "sm", variant: "revert" }),
"rounded-full",
)}
data-slot="toast-action"
>
{toast.actionProps.children}
</Toast.Action>
)}
</Toast.Content>
</Toast.Root>
);
})}
</Toast.Viewport>
</Toast.Portal>
);
}
export { ToastProvider, type ToastPosition, toastManager }; API Reference
ToastProvider
The ToastProvider component extends the Base UI Toast.Provider
props and adds the following:
| Prop | Type | Default | Description |
|---|---|---|---|
| position | "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right" | "bottom-right" | Placement of the toast stack on the screen. |