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