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:

tsx
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:

tsx
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:

tsx
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:

PropTypeDefaultDescription
position"top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right""bottom-right"Placement of the toast stack on the screen.