logo

Nurav UI

Community
Components

Toast

A modern, scalable toast component with progress indicators, multiple variants, and premium animations.Copy Markdown

Preview

The Toast component provides a clean, non-intrusive way to display temporary notifications. It supports multiple states and provides a simple, imperative API.

Variants Showcase

Test out the different visual styles available for the Toast component.

Loading...

Installation

Automatic (CLI)

The fastest way to get started is using the shadcn CLI.

Terminal
npx shadcn@latest add https://nurav-ui.vercel.app/r/toast.json

Manual Setup

If you prefer manual configuration, follow these steps:

Install Dependencies

Install the necessary animation and icon libraries:

Terminal
npm install motion lucide-react clsx tailwind-merge

Create Component

Copy the following code into your project (e.g., components/nurav-ui/Toast.tsx). This file contains the UI, the state management, and the imperative hook.

Toast.tsx
"use client";

import React, {
  useState,
  useEffect,
  useCallback,
  createContext,
  useContext,
} from "react";
import { motion, AnimatePresence } from "motion/react";
import { cn } from "@/lib/utils";
import {
  X,
  CheckCircle2,
  AlertCircle,
  Info,
  AlertTriangle,
  Loader2,
} from "lucide-react";

// --- Types ---

export type ToastVariant =
  | "default"
  | "success"
  | "error"
  | "warning"
  | "info"
  | "loading";

export type ToastPosition =
  | "top-left"
  | "top-right"
  | "bottom-left"
  | "bottom-right";

export interface ToastProps {
  id: string;
  title?: string;
  description?: string;
  variant?: ToastVariant;
  duration?: number;
  position?: ToastPosition;
  onClose: (id: string) => void;
}

// --- Icons Mapping ---

const VARIANT_ICONS: Record<ToastVariant, React.ReactNode> = {
  default: null,
  success: <CheckCircle2 className="w-4 h-4 mt-0.5 text-emerald-500" />,
  error: <AlertCircle className="w-4 h-4 mt-0.5 text-red-500" />,
  warning: <AlertTriangle className="w-4 h-4 mt-0.5 text-amber-500" />,
  info: <Info className="w-4 h-4 mt-0.5 text-blue-500" />,
  loading: (
    <Loader2 className="w-4 h-4 mt-0.5  text-foreground/50 animate-spin" />
  ),
};

// --- Toast Component ---

export const Toast = React.memo(
  React.forwardRef<HTMLDivElement, ToastProps>(
    (
      {
        id,
        title,
        description,
        variant = "default",
        duration = 5000,
        position = "bottom-right",
        onClose,
      },
      ref,
    ) => {
      useEffect(() => {
        if (duration === Infinity) return;
        const timer = setTimeout(() => onClose(id), duration);
        return () => clearTimeout(timer);
      }, [id, duration, onClose]);

      const isLeft = position.includes("left");
      const isTop = position.includes("top");

      return (
        <motion.div
          ref={ref}
          layout
          initial={{
            opacity: 0,
            y: isTop ? -20 : 20,
            x: isLeft ? -20 : 20,
            scale: 0.9,
            filter: "blur(10px)",
          }}
          animate={{ opacity: 1, y: 0, x: 0, scale: 1, filter: "blur(0px)" }}
          exit={{
            opacity: 0,
            x: isLeft ? -40 : 40,
            scale: 0.5,
            filter: "blur(5px)",
            transition: { duration: 0.2 },
          }}
          whileHover={{ scale: 1.02 }}
          className={cn(
            "group relative flex w-full max-w-sm gap-2 overflow-hidden rounded-md border p-3 shadow-sm transition-all",
            "bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl",
            "border-foreground/10 hover:border-foreground/20",
            variant === "success" &&
              "border-foreground/10 shadow-emerald-500/5",
            variant === "error" && "border-foreground/10 shadow-red-500/5",
            variant === "warning" && "border-foreground/10 shadow-amber-500/5",
            variant === "info" && "border-foreground/10 shadow-blue-500/5",
          )}
        >
          {/* Progress Bar (Subtle) */}
          {duration !== Infinity && (
            <motion.div
              initial={{ scaleX: 1 }}
              animate={{ scaleX: 0 }}
              transition={{ duration: duration / 1000, ease: "linear" }}
              className={cn(
                "absolute bottom-0 left-0 h-0.5 w-full origin-left bg-foreground/10",
                variant === "success" && "bg-emerald-500/70",
                variant === "error" && "bg-red-500/70",
                variant === "warning" && "bg-amber-500/70",
                variant === "info" && "bg-blue-500/70",
                variant === "loading" && "bg-foreground/30",
              )}
            />
          )}

          {/* Icon */}
          {VARIANT_ICONS[variant] && (
            <div className="flex shrink-0 items-start pt-0.5">
              {VARIANT_ICONS[variant]}
            </div>
          )}

          {/* Content */}
          <div className="flex flex-1 flex-col gap-1 pr-6">
            {title && (
              <h3 className="text-md font-semibold text-foreground/90">
                {title}
              </h3>
            )}
            {description && (
              <p className="text-sm text-foreground/50  font-medium">
                {description}
              </p>
            )}
          </div>

          {/* Close Button */}
          <button
            onClick={() => onClose(id)}
            className="absolute right-3 top-3 rounded-lg p-1 text-foreground/20 opacity-0 transition-all hover:bg-foreground/5 hover:text-foreground/80 group-hover:opacity-100"
          >
            <X className="w-4 h-4" />
          </button>
        </motion.div>
      );
    },
  ),
);

Toast.displayName = "Toast";

type ToastAction =
  | { type: "ADD_TOAST"; toast: Omit<ToastProps, "onClose"> }
  | { type: "REMOVE_TOAST"; id: string };

interface ToastState {
  toasts: Omit<ToastProps, "onClose">[];
}

const toastReducer = (state: ToastState, action: ToastAction): ToastState => {
  switch (action.type) {
    case "ADD_TOAST":
      return { toasts: [action.toast, ...state.toasts].slice(0, 5) };
    case "REMOVE_TOAST":
      return { toasts: state.toasts.filter((t) => t.id !== action.id) };
    default:
      return state;
  }
};

let memoryState: ToastState = { toasts: [] };
let listeners: React.Dispatch<React.SetStateAction<ToastState>>[] = [];

function dispatch(action: ToastAction) {
  memoryState = toastReducer(memoryState, action);
  listeners.forEach((listener) => listener(memoryState));
}

type ToastOptions = Omit<ToastProps, "id" | "onClose">;

function toastFunction(props: ToastOptions) {
  const id = Math.random().toString(36).substring(2, 9);
  dispatch({ type: "ADD_TOAST", toast: { ...props, id } });
  return id;
}

toastFunction.dismiss = (id: string) => dispatch({ type: "REMOVE_TOAST", id });
toastFunction.success = (title: string, description?: string) =>
  toastFunction({ title, description, variant: "success" });
toastFunction.error = (title: string, description?: string) =>
  toastFunction({ title, description, variant: "error" });
toastFunction.warning = (title: string, description?: string) =>
  toastFunction({ title, description, variant: "warning" });
toastFunction.info = (title: string, description?: string) =>
  toastFunction({ title, description, variant: "info" });
toastFunction.loading = (title: string, description?: string) =>
  toastFunction({ title, description, variant: "loading", duration: Infinity });

export const toast = toastFunction;

type ToastContextType = {
  toast: typeof toast;
  dismiss: typeof toast.dismiss;
};

const ToastContext = createContext<ToastContextType | null>(null);

export function ToastProvider({
  children,
  position = "bottom-right",
}: {
  children: React.ReactNode;
  position?: ToastPosition;
}) {
  return (
    <ToastContext.Provider value={{ toast, dismiss: toast.dismiss }}>
      {children}
      <Toaster position={position} />
    </ToastContext.Provider>
  );
}

export const useToast = () => {
  const context = useContext(ToastContext);
  if (!context) {
    throw new Error("useToast must be used within a ToastProvider");
  }
  return context;
};

// --- Toaster Component ---

export function Toaster({
  position = "bottom-right",
}: {
  position?: ToastPosition;
}) {
  const [state, setState] = useState<ToastState>(memoryState);

  useEffect(() => {
    listeners.push(setState);
    return () => {
      const index = listeners.indexOf(setState);
      if (index > -1) listeners.splice(index, 1);
    };
  }, []);

  const positionClasses = {
    "top-left": "top-0 left-0",
    "top-right": "top-0 right-0",
    "bottom-left": "bottom-0 left-0",
    "bottom-right": "bottom-0 right-0",
  };

  const isLeft = position.includes("left");

  return (
    <div
      className={cn(
        "fixed z-100 flex flex-col gap-3 p-4 sm:p-6 sm:max-w-[420px] pointer-events-none",
        positionClasses[position],
        isLeft ? "items-start" : "items-end",
      )}
    >
      <AnimatePresence mode="popLayout">
        {state.toasts.map((t) => (
          <div key={t.id} className="pointer-events-auto w-full">
            <Toast {...t} position={position} onClose={toast.dismiss} />
          </div>
        ))}
      </AnimatePresence>
    </div>
  );
}

Setup

To use the toast system, you need to wrap your application with the ToastProvider component.

app/layout.tsx
import { ToastProvider } from "@/components/nurav-ui/Toast";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {/* You can specify the position here */}
        <ToastProvider position="top-right">
          {children}
        </ToastProvider>
      </body>
    </html>
  );
}

API Reference

toast()

The main function to trigger a notification.

PropTypeDefaultDescription
titlerequired
string—The main heading of the toast.
description
string—Optional subtext for more context.
variant
ToastVariant—Visual style (default, success, error, etc).
duration
number—Time in ms before toast closes. Use `Infinity` to keep open.

toast() helpers

Direct methods for triggering specific variants.

PropTypeDefaultDescription
toast.success(title, desc)
Function—Triggers a success toast with emerald accents.
toast.error(title, desc)
Function—Triggers an error toast with red accents.
toast.warning(title, desc)
Function—Triggers a warning toast with amber accents.
toast.info(title, desc)
Function—Triggers an info toast with blue accents.
toast.loading(title, desc)
Function—Triggers a persistent toast with a spinner.
toast.dismiss(id)
Function—Manually closes a toast by its ID.

ToastProvider props

PropTypeDefaultDescription
position
ToastPosition'bottom-right'Placement on the screen.
childrenrequired
ReactNode—Your application components.

Examples

Usage with Fetch API

A common use case for toasts is providing feedback during asynchronous operations like data fetching.

const handleUpdateProfile = async (data) => {
  // 1. Show a loading toast and save its ID
  const toastId = toast.loading(
    "Updating profile...", 
    "Please wait while we save your changes."
  );

  try {
    const response = await fetch("/api/user/update", {
      method: "POST",
      body: JSON.stringify(data),
    });

    if (!response.ok) throw new Error("Update failed");

    // 2. On success, dismiss the loader and show success
    toast.dismiss(toastId);
    toast.success("Profile updated!", "Your changes are now live.");
    
  } catch (error) {
    // 3. On error, dismiss the loader and show error
    toast.dismiss(toastId);
    toast.error("Update failed", "Something went wrong. Please try again.");
  }
};

Simple Use Case (onClick)

You can trigger a toast instantly from any interactive element.

import { toast } from "@/components/nurav-ui/Toast";
import { Button } from "@/components/nurav-ui/Button";

export default function CopySection() {
  const handleCopy = () => {
    navigator.clipboard.writeText("https://nurav-ui.com");
    toast.success("Link copied!", "You can now share it with others.");
  };

  return (
    <Button onClick={handleCopy}>
      Copy Link
    </Button>
  );
}

Built by Varun

Last Updated:April 10, 2026

On this page