Toast
A succinct message that is displayed temporarily with auto-dismiss.
Demo
Dependencies
lucide-reactclsxtailwind-merge
Props
This is a base primitive. It accepts standard HTML attributes plus className for styling via cn().
Source
"use client";
import {
useState,
useCallback,
createContext,
useContext,
useEffect,
type ReactNode,
} from "react";
import { cn } from "@/lib/utils";
import { X } from "lucide-react";
interface ToastData {
id: string;
title?: string;
description?: string;
variant?: "success" | "error" | "info";
duration?: number;
}
type AddToastFn = (t: Omit<ToastData, "id">) => void;
let addToastGlobal: AddToastFn | null = null;
let toastId = 0;
export function toast(config: Omit<ToastData, "id">) {
if (addToastGlobal) addToastGlobal(config);
}
const ToastContext = createContext<{
toasts: ToastData[];
removeToast: (id: string) => void;
}>({
toasts: [],
removeToast: () => {},
});
export function ToastProvider({ children }: { children: ReactNode }) {
const [toasts, setToasts] = useState<ToastData[]>([]);
const addToast = useCallback((t: Omit<ToastData, "id">) => {
const id = String(++toastId);
setToasts((prev) => [...prev, { ...t, id }]);
const dur = t.duration ?? 5000;
if (dur > 0) {
setTimeout(() => {
setToasts((prev) => prev.filter((x) => x.id !== id));
}, dur);
}
}, []);
useEffect(() => {
addToastGlobal = addToast;
return () => {
addToastGlobal = null;
};
}, [addToast]);
const removeToast = useCallback((id: string) => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, []);
const variantClass = (v?: string) =>
v === "success"
? "border-green-200 bg-green-50"
: v === "error"
? "border-red-200 bg-red-50"
: "border-gray-200 bg-white";
return (
<ToastContext.Provider value={{ toasts, removeToast }}>
{children}
<div className="fixed bottom-4 right-4 z-[100] flex flex-col gap-2">
{toasts.map((t) => (
<div
key={t.id}
role="alert"
className={cn(
"flex w-80 items-start gap-3 rounded-lg border p-4 shadow-sm",
variantClass(t.variant),
)}
>
<div className="flex-1 space-y-1">
{t.title && (
<p className="text-sm font-medium text-gray-900">{t.title}</p>
)}
{t.description && (
<p className="text-sm text-gray-500">{t.description}</p>
)}
</div>
<button
onClick={() => removeToast(t.id)}
className="shrink-0 text-gray-400 hover:text-gray-600"
>
<X className="h-4 w-4" />
</button>
</div>
))}
</div>
</ToastContext.Provider>
);
}
export function useToast() {
return useContext(ToastContext);
}
CLI
npx genui add toast