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:

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