Toaster
A stacked notification toast component with smooth animations.
Toaster
Preview
Source Code
Source Code
tsx
"use client";
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "motion/react";
import { X, CheckCircle2, AlertCircle, Info } from "lucide-react";
import { cn } from "@/lib/utils";
type ToastType = "success" | "error" | "info";
interface Toast {
id: string;
message: string;
type: ToastType;
icon?: React.ReactNode;
}
const ToastItem = ({
toast,
removeToast,
}: {
toast: Toast;
removeToast: (id: string) => void;
}) => {
useEffect(() => {
const timer = setTimeout(() => {
removeToast(toast.id);
}, 4000);
return () => clearTimeout(timer);
}, [toast.id, removeToast]);
return (
<motion.div
layout
initial={{ opacity: 0, y: 50, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, scale: 0.9, transition: { duration: 0.2 } }}
className={cn(
"pointer-events-auto flex w-full max-w-sm items-center gap-3 rounded-md border border-white/10 bg-white/80 p-4 backdrop-blur-md dark:bg-zinc-900/80",
"dark:border-zinc-800",
)}
>
<div
className={cn("flex h-8 w-8 items-center justify-center rounded-full", {
"bg-green-500/10 text-green-600 dark:text-green-500":
toast.type === "success",
"bg-red-500/10 text-red-600 dark:text-red-500":
toast.type === "error",
"bg-blue-500/10 text-blue-600 dark:text-blue-500":
toast.type === "info",
})}
>
{toast.icon ? (
toast.icon
) : (
<>
{toast.type === "success" && (
<CheckCircle2 className={cn("h-5 w-5")} />
)}
{toast.type === "error" && (
<AlertCircle className={cn("h-5 w-5")} />
)}
{toast.type === "info" && <Info className={cn("h-5 w-5")} />}
</>
)}
</div>
<div className={cn("flex-1")}>
<h3
className={cn("text-sm font-medium text-zinc-900 dark:text-zinc-100")}
>
{toast.type.charAt(0).toUpperCase() + toast.type.slice(1)}
</h3>
<p className={cn("text-sm text-zinc-500 dark:text-zinc-400")}>
{toast.message}
</p>
</div>
<button
onClick={() => removeToast(toast.id)}
className={cn(
"cursor-pointer text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200",
)}
>
<X className={cn("h-5 w-5")} />
</button>
</motion.div>
);
};
const positionClasses = {
"top-left":
"items-start justify-start sm:items-start sm:justify-start flex-col",
"top-center":
"items-center justify-start sm:items-center sm:justify-start flex-col",
"top-right":
"items-center justify-start sm:items-end sm:justify-start flex-col",
"bottom-left":
"items-start justify-start sm:items-start sm:justify-start flex-col-reverse",
"bottom-center":
"items-center justify-start sm:items-center sm:justify-start flex-col-reverse",
"bottom-right":
"items-center justify-start sm:items-end sm:justify-start flex-col-reverse",
};
export interface ToasterProps {
toasts: Toast[];
removeToast: (id: string) => void;
position?: keyof typeof positionClasses;
}
export const Toaster = ({
toasts,
removeToast,
position = "top-right",
}: ToasterProps) => {
return (
<div
className={cn(
"pointer-events-none absolute inset-0 flex gap-2 p-4",
positionClasses[position],
)}
>
{/* <AnimatePresence> */}
{toasts.map((toast) => (
<ToastItem key={toast.id} toast={toast} removeToast={removeToast} />
))}
{/* </AnimatePresence> */}
</div>
);
};
const ToasterDemo = () => {
const [toasts, setToasts] = useState<Toast[]>([]);
const addToast = (
type: ToastType,
message: string,
icon?: React.ReactNode,
) => {
const id = Math.random().toString(36).substring(7);
setToasts((prev) => [...prev, { id, message, type, icon }]);
};
const removeToast = (id: string) => {
setToasts((prev) => prev.filter((toast) => toast.id !== id));
};
return (
<div
className={cn(
"relative flex min-h-[400px] w-full flex-col items-center justify-center gap-4 overflow-hidden rounded-md border border-zinc-200 bg-zinc-50 p-8 dark:border-zinc-800 dark:bg-zinc-950/50",
)}
>
<div className={cn("flex flex-wrap gap-4")}>
<button
onClick={() =>
addToast(
"success",
"Changes saved successfully!",
<CheckCircle2 className={cn("h-5 w-5")} />,
)
}
className={cn(
"cursor-pointer rounded-md bg-white px-4 py-2 text-sm font-medium text-zinc-900 shadow-sm ring-1 ring-zinc-200 transition-all hover:bg-zinc-50 dark:bg-zinc-900 dark:text-zinc-100 dark:ring-zinc-800 dark:hover:bg-zinc-800",
)}
>
Add Success Toast
</button>
<button
onClick={() =>
addToast(
"error",
"Failed to save changes.",
<AlertCircle className={cn("h-5 w-5")} />,
)
}
className={cn(
"cursor-pointer rounded-md bg-white px-4 py-2 text-sm font-medium text-zinc-900 shadow-sm ring-1 ring-zinc-200 transition-all hover:bg-zinc-50 dark:bg-zinc-900 dark:text-zinc-100 dark:ring-zinc-800 dark:hover:bg-zinc-800",
)}
>
Add Error Toast
</button>
<button
onClick={() =>
addToast(
"info",
"New update available.",
<Info className={cn("h-5 w-5")} />,
)
}
className={cn(
"cursor-pointer rounded-md bg-white px-4 py-2 text-sm font-medium text-zinc-900 shadow-sm ring-1 ring-zinc-200 transition-all hover:bg-zinc-50 dark:bg-zinc-900 dark:text-zinc-100 dark:ring-zinc-800 dark:hover:bg-zinc-800",
)}
>
Add Info Toast
</button>
</div>
<Toaster toasts={toasts} removeToast={removeToast} position="top-right" />
</div>
);
};
export default ToasterDemo;
Share this post