This document outlines the steps to create Toast
component styled withTailwind CSS and using some npm dependency libraries.
Node.js
and npm
installed on your machine.Tailwind CSS
installed in your project.CVA(class-variance-authority)
is a utility for managing CSS class names based on various conditions.clsx
is a tiny utility for constructing className strings conditionally.@radix-ui/react-portal
is a minimalist JavaScript library that renders a React subtree in a different part of the DOM.zustand
A small, fast, and scalable bear bones state management solution1// tailwind.config.js
2module.exports = {
3 ...
4 theme: {
5 ...
6 extend: {
7 ...
8 keyframes: {
9 "toast-in": {
10 from: { opacity: "0", transform: "scale(0.9)" },
11 to: { opacity: "1", transform: "scale(1)" },
12 },
13 "toast-out": {
14 from: { opacity: "1", transform: "scale(1)" },
15 to: { opacity: "0", transform: "scale(0.9)" },
16 },
17 },
18 animation: {
19 "toast-in": "toast-in 0.15s ease-in-out",
20 "toast-out": "toast-out 0.15s ease-in-out",
21 },
22 },
23 },
24 plugins: [require("tailwindcss-animate")]
25};
26
1// use-toast.ts
2import { create } from "zustand";
3
4interface ToastOptions {
5 message: string;
6 type: "success" | "error" | "warning" | "info";
7}
8
9interface ToastData extends ToastOptions {
10 id: number;
11}
12
13interface StoreProps {
14 data: ToastData | null;
15 showToast: (data: ToastOptions) => void;
16 clearToast: () => void;
17}
18
19export const useToast = create<StoreProps>((set) => ({
20 data: null,
21 showToast: (data: ToastOptions) => set({ data: { ...data, id: Date.now() } }),
22 clearToast: () => set({ data: null }),
23}));
24
1// toast.component.tsx
2import clsx from "clsx";
3import * as Portal from "@radix-ui/react-portal";
4import { cva } from "class-variance-authority";
5import {
6 CircleAlert,
7 CircleCheckBig,
8 CircleX,
9 TriangleAlert,
10} from "lucide-react";
11import { useCallback } from "react";
12import { useToast } from "./use-toast";
13
14const toastVariants = cva(
15 "z-20 fixed transform bg-white rounded-md border border-neutral-200 dark:border-neutral-700 shadow-sm flex items-center pl-4 pr-6 py-4 animate-toast-in fill-mode-forwards",
16 {
17 variants: {
18 position: {
19 topLeft: "top-4 left-4",
20 topCenter: "top-4 left-1/2 -translate-x-1/2",
21 topRight: "top-4 right-4",
22 bottomLeft: "bottom-4 left-4",
23 bottomCenter: "bottom-4 left-1/2 -translate-x-1/2",
24 bottomRight: "bottom-4 right-4",
25 },
26 },
27 defaultVariants: {
28 position: "topRight",
29 },
30 },
31);
32
33export interface ToastProps extends VariantProps<typeof toastVariants> {
34 className?: string;
35 textClassName?: string;
36}
37
38export type ToastPosition = ToastProps["position"];
39
40const Toast: React.FC<ToastProps> = ({
41 position,
42 className,
43 textClassName,
44}) => {
45 const { data, clearToast } = useToast();
46
47 const refCallback = useCallback((el: HTMLDivElement | null) => {
48 if (!el) return;
49
50 setTimeout(() => {
51 el.classList.add("animate-toast-out");
52 }, 3000);
53 }, []);
54
55 const handleAnimationEnd = (e: React.AnimationEvent<HTMLDivElement>) => {
56 if (e.animationName === "toast-out") clearToast();
57 };
58
59 return (
60 <Portal.Root>
61 {data && (
62 <div
63 key={data.id}
64 ref={refCallback}
65 className={cn(
66 toastVariants({ position }),
67 {
68 "bg-green-600 text-white": data.type === "success",
69 "bg-red-600 text-white": data.type === "error",
70 "bg-yellow-600 text-white": data.type === "warning",
71 "bg-cyan-600 text-white": data.type === "info",
72 },
73 className,
74 )}
75 onAnimationEnd={handleAnimationEnd}
76 >
77 {data.type === "success" ? (
78 <CircleCheckBig className="h-6 w-6" />
79 ) : data.type === "error" ? (
80 <CircleX className="h-6 w-6" />
81 ) : data.type === "warning" ? (
82 <TriangleAlert className="h-6 w-6" />
83 ) : data.type === "info" ? (
84 <CircleAlert className="h-6 w-6" />
85 ) : null}
86 <p className={cn("ml-4 mt-[2px]", textClassName)}>{data.message}</p>
87 </div>
88 )}
89 </Portal.Root>
90 );
91};
92
93export default Toast;
94