Search

Ctrl + K

Dialog

This document outlines the steps to create Dialog 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.
Step 1: Create Tailwind animation
1// tailwind.config.js
2module.exports = {
3  ...
4  theme: {
5    ...
6    extend: {
7      ...
8      keyframes: {
9        "opacity-up": {
10          from: { opacity: 0 },
11          to: { opacity: 1 },
12        },
13        "opacity-down": {
14          from: { opacity: 1 },
15          to: { opacity: 0 },
16        },
17        "scale-up": {
18          from: { transform: "scale(0.9)", opacity: 0 },
19          to: { transform: "scale(1)", opacity: 1 },
20        },
21        "scale-down": {
22          from: { transform: "scale(1)", opacity: 1 },
23          to: { transform: "scale(0.9)", opacity: 0 },
24        },
25      },
26      animation: {
27        "opacity-up": "opacity-up 0.15s ease-in-out",
28        "opacity-down": "opacity-down 0.15s ease-in-out",
29        "scale-up": "scale-up 0.15s ease-in-out",
30        "scale-down": "scale-down 0.15s ease-in-out",
31      },
32    },
33  },
34  plugins: [require("tailwindcss-animate")]
35};
36
Step 2: Create Dialog component
1// dialog.component.tsx
2import clsx from "clsx";
3import * as Portal from "@radix-ui/react-portal";
4import { XIcon } from "lucide-react";
5import { useEffect, useState } from "react";
6
7interface DialogProps {
8  open: boolean;
9  hideHeaderClose?: boolean;
10  overlayCancel?: boolean;
11  disabledAnimation?: boolean;
12  title?: string;
13  hasFooterCancel?: boolean;
14  hasFooterConfirm?: boolean;
15  confirmTitle?: string;
16  className?: string;
17  children: React.ReactNode;
18  onConfirm?: () => void;
19  onClose: () => void;
20}
21
22const Dialog: React.FC<DialogProps> = ({
23  open,
24  title,
25  hideHeaderClose,
26  overlayCancel,
27  disabledAnimation,
28  hasFooterCancel,
29  hasFooterConfirm,
30  confirmTitle,
31  className,
32  children,
33  onConfirm,
34  onClose,
35}) => {
36  const [rendering, setRendering] = useState<boolean>(open);
37
38  useEffect(() => {
39    if (open) {
40      setRendering(true);
41    } else if (disabledAnimation) {
42      setRendering(false);
43    }
44  }, [open, disabledAnimation]);
45
46  const handleAnimationEnd = () => {
47    if (disabledAnimation) return;
48    if (!open) setRendering(false);
49  };
50
51  const handleClickOverlay = () => {
52    if (!overlayCancel) return;
53    onClose();
54  };
55
56  return (
57    <Portal.Root>
58      <div
59        className={cn(
60          "fixed inset-0 z-20 flex items-center justify-center",
61          {
62            invisible: !rendering,
63          },
64          className,
65        )}
66      >
67        <div
68          className={cn(
69            "absolute inset-0 bg-[rgba(0,0,0,0.5)] fill-mode-forwards",
70            !disabledAnimation &&
71              (open ? "animate-opacity-up" : "animate-opacity-down"),
72          )}
73          onClick={handleClickOverlay}
74        />
75        <div
76          className={cn(
77            "z-10 min-h-40 min-w-96 rounded-md bg-white shadow-md fill-mode-forwards dark:bg-neutral-800",
78            !disabledAnimation &&
79              (open ? "animate-scale-up" : "animate-scale-down"),
80          )}
81          onAnimationEnd={handleAnimationEnd}
82        >
83          {(title || !hideHeaderClose) && (
84            <div
85              className={cn("flex items-center rounded-t-md px-6 py-4", {
86                "justify-between": title && !hideHeaderClose,
87                "justify-end": !title && !hideHeaderClose,
88              })}
89            >
90              {title && <p className="text-xl font-semibold">{title}</p>}
91              {!hideHeaderClose && (
92                <XIcon
93                  className="h-6 w-6 cursor-pointer text-neutral-700 dark:text-neutral-200"
94                  onClick={onClose}
95                />
96              )}
97            </div>
98          )}
99          <div className="px-6 py-4">{children}</div>
100          {(hasFooterConfirm || hasFooterCancel) && (
101            <div className="flex justify-end gap-3 px-6 py-4">
102              {hasFooterCancel && (
103                <button
104                  className="rounded-sm border-none bg-neutral-200 px-4 py-2 text-sm text-neutral-600 focus:outline-none dark:bg-neutral-500 dark:text-neutral-200"
105                  onClick={onClose}
106                >
107                  Cancel
108                </button>
109              )}
110              {hasFooterConfirm && (
111                <button
112                  className="rounded-sm border-none bg-neutral-800 px-4 py-2 text-sm text-white focus:outline-none dark:bg-neutral-200 dark:text-neutral-900"
113                  onClick={onConfirm}
114                >
115                  {confirmTitle || "Confirm"}
116                </button>
117              )}
118            </div>
119          )}
120        </div>
121      </div>
122    </Portal.Root>
123  );
124};
125
126export default Dialog;
127