Toast
A transient notification stack for status updates, actions, and async feedback.
ToastExample
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.
ToastVariantsExample
Position
Pass the position prop to ToastProvider to place the stack at any screen edge or center.
ToastPositionExample
Promise states
Use toastManager.promise(...) to keep loading, success, and error feedback tied to the same async flow.
ToastPromiseExample
Usage
Mount the provider once:
import { ToastProvider } from "registry/default/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/default/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 "@/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. |