This document outlines the steps to create Drawer
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 "shift-right-in": {
18 from: { transform: "translateX(100%)" },
19 to: { transform: "translateX(0%)" },
20 },
21 "shift-right-out": {
22 from: { transform: "translateX(0%)" },
23 to: { transform: "translateX(100%)" },
24 },
25 "shift-left-in": {
26 from: { transform: "translateX(-100%)" },
27 to: { transform: "translateX(0%)" },
28 },
29 "shift-left-out": {
30 from: { transform: "translateX(0%)" },
31 to: { transform: "translateX(-100%)" },
32 },
33 },
34 animation: {
35 "opacity-up": "opacity-up 0.15s ease-in-out",
36 "opacity-down": "opacity-down 0.15s ease-in-out",
37 "shift-right-in": "shift-right-in 0.15s ease-in-out",
38 "shift-right-out": "shift-right-out 0.15s ease-in-out",
39 "shift-left-in": "shift-left-in 0.15s ease-in-out",
40 "shift-left-out": "shift-left-out 0.15s ease-in-out",
41 },
42 },
43 },
44 plugins: [require("tailwindcss-animate")]
45};
46
1// drawer.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 DrawerProps {
8 open: boolean;
9 position?: Position;
10 hideHeaderClose?: boolean;
11 overlayCancel?: boolean;
12 title?: string;
13 hasFooterCancel?: boolean;
14 hasFooterConfirm?: boolean;
15 confirmTitle?: string;
16 className?: string;
17 wrapperClassName?: string;
18 children: React.ReactNode;
19 onConfirm?: () => void;
20 onClose: () => void;
21}
22
23const Drawer: React.FC<DrawerProps> = ({
24 open,
25 position = "right",
26 hideHeaderClose,
27 overlayCancel,
28 title,
29 hasFooterCancel,
30 hasFooterConfirm,
31 confirmTitle,
32 className,
33 wrapperClassName,
34 children,
35 onConfirm,
36 onClose,
37}) => {
38 const [rendering, setRendering] = useState<boolean>(open);
39
40 useEffect(() => {
41 if (open) {
42 setRendering(true);
43 }
44 }, [open]);
45
46 const handleAnimationEnd = () => {
47 if (!open) setRendering(false);
48 };
49
50 return (
51 <Portal.Root>
52 <div
53 className={cn(
54 "fixed inset-0 z-20",
55 {
56 invisible: !rendering,
57 },
58 className,
59 )}
60 >
61 <div
62 className={cn(
63 "absolute inset-0 bg-[rgba(0,0,0,0.5)] fill-mode-forwards",
64 open ? "animate-opacity-up" : "animate-opacity-down",
65 )}
66 onClick={() => overlayCancel && onClose()}
67 />
68 <div
69 className={cn(
70 "absolute inset-y-0 z-10 flex w-96 flex-col overflow-auto bg-white shadow-md fill-mode-forwards dark:bg-neutral-800",
71 {
72 "right-0": position === "right",
73 "left-0": position === "left",
74 },
75 {
76 "animate-shift-right-in": open && position === "right",
77 "animate-shift-right-out": !open && position === "right",
78 "animate-shift-left-in": open && position === "left",
79 "animate-shift-left-out": !open && position === "left",
80 },
81 )}
82 onAnimationEnd={handleAnimationEnd}
83 >
84 {(title || !hideHeaderClose) && (
85 <div className="flex h-12 items-center justify-between border-b border-neutral-200 bg-white px-2 dark:border-neutral-700 dark:bg-neutral-800">
86 <h3 className="line-clamp-1 text-ellipsis font-semibold">
87 {title}
88 </h3>
89 {!hideHeaderClose && (
90 <XIcon
91 className="h-6 w-6 cursor-pointer text-neutral-700 dark:text-neutral-200"
92 onClick={onClose}
93 />
94 )}
95 </div>
96 )}
97 <div className={cn("flex-1 overflow-auto", wrapperClassName)}>
98 {children}
99 </div>
100 {(hasFooterConfirm || hasFooterCancel) && (
101 <div className="sticky bottom-0 flex justify-end gap-3 border-t border-neutral-200 bg-white px-4 py-3 dark:border-neutral-700 dark:bg-neutral-800">
102 {hasFooterCancel && (
103 <button
104 className="rounded-md 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-md 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 Drawer;
127