Search

Ctrl + K

Accordion

This document outlines the steps to create Accordion 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.
Step 1: Create Accordion component
1// accordion-component.tsx
2import clsx from "clsx";
3import { createContext, useState } from "react";
4
5type AccordionType = "light" | "bordered" | "splitted";
6
7interface ContextProps {
8  variant: AccordionType;
9  itemClassName?: string;
10  headerClassName?: string;
11  contentClassName?: string;
12  openingItems: string[];
13  toggleItem: (key: string) => void;
14}
15
16export const AccordionContext = createContext<ContextProps>({
17  variant: "light",
18  openingItems: [],
19  toggleItem: () => {},
20});
21
22interface Props {
23  variant?: AccordionType;
24  selectMode?: "single" | "multiple";
25  className?: string;
26  itemClassName?: string;
27  headerClassName?: string;
28  contentClassName?: string;
29  children: React.ReactNode;
30}
31
32const Accordion: React.FC<Props> = ({
33  variant = "light",
34  selectMode = "single",
35  className,
36  itemClassName,
37  headerClassName,
38  contentClassName,
39  children,
40}) => {
41  const [openingItems, setOpeningItems] = useState<string[]>([]);
42
43  const toggleItem = (key: string) => {
44    if (selectMode === "single") {
45      if (openingItems.includes(key)) setOpeningItems([]);
46      else setOpeningItems([key]);
47    } else {
48      if (openingItems.includes(key))
49        setOpeningItems(openingItems.filter((item) => item !== key));
50      else setOpeningItems([...openingItems, key]);
51    }
52  };
53
54  return (
55    <AccordionContext.Provider
56      value={{
57        variant,
58        itemClassName,
59        headerClassName,
60        contentClassName,
61        openingItems,
62        toggleItem,
63      }}
64    >
65      <div
66        className={clsx(
67          {
68            "rounded-lg border border-neutral-200 px-4 py-2 shadow-sm":
69              variant === "bordered",
70            "grid grid-cols-1 gap-2": variant === "splitted",
71          },
72          className,
73        )}
74      >
75        {children}
76      </div>
77    </AccordionContext.Provider>
78  );
79};
80
81export default Accordion;
82
Step 2: Create AccordionItem component
1// accordion-item.tsx
2import clsx from "clsx";
3import { ChevronLeftIcon } from "lucide-react";
4import { useContext, useEffect, useRef } from "react";
5import { AccordionContext } from "./accordion-component";
6
7interface Props {
8  id: string;
9  title: string;
10  children: React.ReactNode;
11}
12
13const AccordionItem: React.FC<AccordionItemProps> = ({
14  id,
15  title,
16  children,
17}) => {
18  const {
19    variant,
20    itemClassName,
21    headerClassName,
22    contentClassName,
23    openingItems,
24    toggleItem,
25  } = useContext(AccordionContext);
26
27  const contentRef = useRef<HTMLDivElement | null>(null);
28  const contentHeight = useRef<number>(0);
29
30  useEffect(() => {
31    const observer = new ResizeObserver((entries) => {
32      entries.forEach((entry) => {
33        contentHeight.current = entry.contentRect.height;
34      });
35    });
36    observer.observe(contentRef.current!);
37    return () => {
38      observer.disconnect();
39    };
40  }, []);
41
42  const opening = openingItems.includes(id);
43
44  return (
45    <div
46      className={clsx(
47        "group",
48        {
49          "rounded-md border border-neutral-200 px-2 shadow-sm":
50            variant === "splitted",
51        },
52        itemClassName
53      )}
54    >
55      <div
56        className={clsx("flex cursor-pointer py-2", headerClassName)}
57        onClick={() => toggleItem(id)}
58      >
59        <div className="mr-2 flex flex-1 text-lg font-medium">{title}</div>
60        <ChevronLeftIcon
61          className={clsx(
62            "h-6 w-6 flex-shrink-0 transform text-neutral-700 duration-150",
63            { "-rotate-90": opening }
64          )}
65        />
66      </div>
67      <div
68        className={clsx(
69          "transform overflow-hidden py-0 opacity-0 transition-all duration-300",
70          { "py-2 opacity-100": opening },
71          contentClassName
72        )}
73        style={{
74          height: opening ? `${contentHeight.current + 16}px` : 0,
75        }}
76      >
77        {children}
78      </div>
79      <div className="h-0">
80        <div ref={contentRef} className="invisible py-2">
81          {children}
82        </div>
83      </div>
84      <div
85        className={clsx(
86          "h-[1px] w-full bg-neutral-200 group-last-of-type:hidden",
87          { hidden: variant === "splitted" }
88        )}
89      />
90    </div>
91  );
92};
93