eo-n/ui
UIComponentsToastNew

Toast

Briefly displays transient messages or notifications.

Installation

CLI

npx shadcn@latest add "https://eo-n.vercel.app/r/toast"

Manual

Copy and paste the following code into your project.

"use client";
 
import * as React from "react";
import { Toast as ToastPrimitive } from "@base-ui-components/react/toast";
import { cva, type VariantProps } from "class-variance-authority";
import {
  CircleAlert,
  CircleCheck,
  CircleHelp,
  Loader,
  TriangleAlert,
  XIcon,
} from "lucide-react";
 
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
 
const toastVariants = cva(
  "group absolute z-[calc(1000-var(--toast-index))] mr-0 grid w-full [transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+calc(var(--toast-index)*var(--translate-y))))_scale(calc(1-(var(--toast-index)*0.07)))] grid-cols-1 rounded-lg border px-3.5 py-4 shadow-lg transition-all [transition-property:opacity,transform,bottom,top] duration-450 ease-out select-none after:absolute after:bottom-full after:left-0 after:h-[calc(1rem+1px)] after:w-full after:content-[''] has-data-[slot=toast-action]:grid-cols-[1fr_auto] data-[ending-style]:opacity-0 data-[expanded]:[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-offset-y)*var(--invert)+calc(var(--toast-index)*1rem*var(--invert))+var(--toast-swipe-movement-y)))] data-[starting-style]:[transform:translateY(var(--starting-style))] data-[ending-style]:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))] data-[expanded]:data-[ending-style]:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))] data-[ending-style]:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-[expanded]:data-[ending-style]:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] data-[ending-style]:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-[expanded]:data-[ending-style]:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] data-[ending-style]:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))] data-[expanded]:data-[ending-style]:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))] data-[ending-style]:[&:not([data-limited])]:[transform:translateY(150%)] [&>*]:transition-opacity [&>*]:duration-450 [&>*]:ease-out not-data-[expanded]:[&>*]:opacity-[calc(1-var(--toast-index))]",
  {
    variants: {
      variant: {
        default: "bg-popover text-popover-foreground border-border",
        loading: "bg-popover text-popover-foreground border-border",
        success:
          "data-[rich-colors=true]:border-green-200 data-[rich-colors=true]:bg-green-100 data-[rich-colors=true]:text-green-800 dark:data-[rich-colors=true]:border-green-900/40 dark:data-[rich-colors=true]:bg-green-950 dark:data-[rich-colors=true]:text-green-300 data-[rich-colors=true]:[&_[data-slot=toast-close]]:bg-green-100 dark:data-[rich-colors=true]:[&_[data-slot=toast-close]]:bg-green-950",
        error:
          "data-[rich-colors=true]:border-red-200 data-[rich-colors=true]:bg-red-100 data-[rich-colors=true]:text-red-800 dark:data-[rich-colors=true]:border-red-900/40 dark:data-[rich-colors=true]:bg-red-950 dark:data-[rich-colors=true]:text-red-300 data-[rich-colors=true]:[&_[data-slot=toast-close]]:bg-red-100 dark:data-[rich-colors=true]:[&_[data-slot=toast-close]]:bg-red-950",
        warning:
          "data-[rich-colors=true]:border-amber-200 data-[rich-colors=true]:bg-amber-100 data-[rich-colors=true]:text-amber-800 dark:data-[rich-colors=true]:border-amber-900/40 dark:data-[rich-colors=true]:bg-amber-950 dark:data-[rich-colors=true]:text-amber-300 data-[rich-colors=true]:[&_[data-slot=toast-close]]:bg-amber-100 dark:data-[rich-colors=true]:[&_[data-slot=toast-close]]:bg-amber-950",
        info: "data-[rich-colors=true]:border-blue-200 data-[rich-colors=true]:bg-blue-100 data-[rich-colors=true]:text-blue-800 dark:data-[rich-colors=true]:border-blue-900/40 dark:data-[rich-colors=true]:bg-blue-950 dark:data-[rich-colors=true]:text-blue-300 data-[rich-colors=true]:[&_[data-slot=toast-close]]:bg-blue-100 dark:data-[rich-colors=true]:[&_[data-slot=toast-close]]:bg-blue-950",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
);
 
export type ToastVariants = VariantProps<typeof toastVariants>;
 
export type ToastType = "loading" | "success" | "error" | "warning" | "info";
 
type Position =
  | "top-center"
  | "top-right"
  | "top-left"
  | "bottom-center"
  | "bottom-right"
  | "bottom-left";
 
const toastManager = ToastPrimitive.createToastManager();
 
const useToastManager = ToastPrimitive.useToastManager;
 
interface ToastProviderProps
  extends React.ComponentProps<typeof ToastPrimitive.Provider> {
  position?: Position;
  richColors?: boolean;
  closeButton?: boolean;
}
 
function ToastProvider({
  position = "bottom-right",
  closeButton = false,
  richColors = false,
  children,
  ...props
}: ToastProviderProps) {
  return (
    <ToastPrimitive.Provider data-slot="toast-provider" {...props}>
      {children}
      <ToastPrimitive.Viewport
        data-slot="toast-viewport"
        data-position={position}
        className="fixed z-50 w-[calc(100%-32px)] data-[position=bottom-center]:bottom-4 data-[position=bottom-center]:left-1/2 data-[position=bottom-center]:-translate-x-1/2 data-[position=bottom-left]:bottom-4 data-[position=bottom-left]:left-4 data-[position=bottom-right]:right-4 data-[position=bottom-right]:bottom-4 data-[position=top-center]:top-4 data-[position=top-center]:left-1/2 data-[position=top-center]:-translate-x-1/2 data-[position=top-left]:top-4 data-[position=top-left]:left-4 data-[position=top-right]:top-4 data-[position=top-right]:right-4 sm:w-full sm:max-w-[370px] sm:data-[position=bottom-center]:bottom-8 sm:data-[position=bottom-left]:bottom-8 sm:data-[position=bottom-left]:left-8 sm:data-[position=bottom-right]:right-8 sm:data-[position=bottom-right]:bottom-8 sm:data-[position=top-center]:top-8 sm:data-[position=top-left]:top-8 sm:data-[position=top-left]:left-8 sm:data-[position=top-right]:top-8 sm:data-[position=top-right]:right-8"
      >
        <ToastLists
          position={position}
          closeButton={closeButton}
          richColors={richColors}
        />
      </ToastPrimitive.Viewport>
    </ToastPrimitive.Provider>
  );
}
 
interface ToastListsProps {
  position: Position;
  richColors: boolean;
  closeButton?: boolean;
}
 
function ToastLists({ position, richColors, closeButton }: ToastListsProps) {
  const { toasts } = useToastManager();
 
  const swipeDirectionMap: Record<
    Position,
    NonNullable<ToastPrimitive.Root.Props["swipeDirection"]>
  > = {
    "bottom-center": "down",
    "bottom-left": ["down", "left"],
    "bottom-right": ["down", "right"],
    "top-center": "up",
    "top-left": ["up", "left"],
    "top-right": ["up", "right"],
  };
 
  const swipeDirection = swipeDirectionMap[position] ?? ["down", "right"];
 
  const icons: Record<ToastType, React.ReactNode> = {
    loading: <Loader className="animate-spin" />,
    success: <CircleCheck />,
    error: <CircleAlert />,
    warning: <TriangleAlert />,
    info: <CircleHelp />,
  };
 
  return toasts.map((toast, i) => (
    <ToastPrimitive.Root
      key={toast.id}
      data-slot="toast"
      data-rich-colors={richColors}
      toast={toast}
      swipeDirection={swipeDirection}
      className={cn(
        toastVariants({ variant: toast.type as ToastVariants["variant"] }),
        position.startsWith("bottom")
          ? "bottom-0 data-[expanded]:bottom-2.5"
          : "top-0 data-[expanded]:top-2.5"
      )}
      style={
        {
          "--gap": "1rem",
          "--translate-y": position.startsWith("bottom") ? "-15px" : "15px",
          "--starting-style": position.startsWith("bottom") ? "150%" : "-150%",
          "--invert": position.startsWith("bottom") ? "-1" : "1",
          "--offset-y":
            "calc(var(--toast-offset-y) * -1 + (var(--toast-index) * 1rem * -1) + var(--toast-swipe-movement-y))",
        } as React.CSSProperties
      }
    >
      {closeButton && (
        <ToastPrimitive.Close
          data-slot="toast-close"
          className={cn(
            "ring-offset-background focus:ring-ring bg-background absolute -top-2 -left-2 rounded-full border p-[2px] opacity-70 transition-opacity hover:opacity-100 focus:ring-1 focus:ring-offset-1 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[14px]",
            {
              "opacity-0 transition-opacity duration-300 ease-out group-data-[expanded]:opacity-100":
                i > 0,
            }
          )}
          aria-label="Close"
        >
          <XIcon />
          <span className="sr-only">Close</span>
        </ToastPrimitive.Close>
      )}
      <div className="flex items-center space-x-2.5 [&>svg]:size-4.5 [&>svg]:shrink-0">
        {toast.type && toast.type ? icons[toast.type as ToastType] : null}
        <div className="flex flex-col">
          <ToastPrimitive.Title
            data-slot="toast-title"
            className="text-sm font-medium tracking-tight"
          />
          <ToastPrimitive.Description
            data-slot="toast-description"
            className="text-muted-foreground text-sm font-normal"
          />
        </div>
      </div>
      <div className="flex items-center justify-between">
        <ToastPrimitive.Action
          data-slot="toast-action"
          render={<Button size="sm" />}
        />
      </div>
    </ToastPrimitive.Root>
  ));
}
 
export { ToastProvider, useToastManager, toastManager };
Update the import paths to match your project setup.

Usage

Add the ToastProvider to your app layout.

layout.tsx
import { ToastProvider } from "@/components/ui/toast";
 
<html lang="en">
  <body>
    <ToastProvider>
      {children}
    </ToastProvider>
  </body>
</html>
import { useToastManager } from "@/components/ui/toast";
const toast = useToastManager()
 
<Button
  onClick={() => {
    toast.add({
      title: "This is a toast",
    });
  }}
>
  Show Toast
</Button>

Examples

With Action

Promise

API Reference

Provider

Provides settings for how toasts appear and behave.

PropTypeDefault
position
"top-center" | "top-right" | "top-left" | "bottom-center" | "bottom-right" | "bottom-left"
bottom-right
richColors
boolean
false
closeButton
boolean
false
timeout
number
5000
limit
number
3
toastManager
ToastManager
-

On this page