This document outlines the steps to create Dialog
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.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
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