Search

Ctrl + K

Drawer

This document outlines the steps to create Drawer 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        "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
Step 2: Create Drawer component
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