Search

Ctrl + K

Toast

This document outlines the steps to create Toast component styled withTailwind CSS and using some npm dependency libraries.

Prerequisites

  • 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 solution
Step 1: Create Tailwind config
1// 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
Step 2: Create useToast hook
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
Step 3: Create Toast component
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