Docs
Accordion

Accordion

A vertically stacked set of interactive headings that each reveal a section of content.

Usage

Installation

Accordion.tsx

1"use client"
2import React, {
3  useState,
4  useRef,
5  createContext,
6  useContext,
7  ReactNode,
8  useLayoutEffect,
9} from "react";
10import { cn } from "@/utils/cn";
11import { motion, AnimatePresence, Spring } from "framer-motion";
12import { ClassNameValue } from "tailwind-merge";
13import { ChevronDown } from "lucide-react";
14
15type AccordionContextType = {
16  activeIndex: number | null;
17  toggleAccordion: (index: number) => void;
18  itemVariant: "default" | "bordered" | "rounded" | "rectangle" | " ";
19  borderVariant?: boolean;
20};
21
22const AccordionContext = createContext<AccordionContextType | null>(null);
23
24interface AccordionProps {
25  children: ReactNode;
26  className?: ClassNameValue;
27  variant?: "default" | "rounded" | "rectangle";
28  border?: boolean;
29}
30
31const Accordion: React.FC<AccordionProps> = ({
32  children,
33  className,
34  variant,
35  border,
36}) => {
37  const [activeIndex, setActiveIndex] = useState<number | null>(null);
38  const toggleAccordion = (index: number) => {
39    setActiveIndex(activeIndex === index ? null : index);
40  };
41  const itemVariant = variant || " ";
42  const borderVariant = border || false;
43  return (
44    <AccordionContext.Provider
45      value={{ activeIndex, toggleAccordion, itemVariant, borderVariant }}
46    >
47      <div
48        className={cn(
49          "flex h-max flex-col items-center justify-center font-geist",
50          className,
51        )}
52      >
53        {children}
54      </div>
55    </AccordionContext.Provider>
56  );
57};
58
59interface AccordionItemProps {
60  index: number;
61  children: React.ReactNode;
62  className?: ClassNameValue;
63}
64
65const springConfig: Spring = {
66  type: "spring",
67  stiffness: 250,
68  damping: 25,
69};
70
71const AccordionItem: React.FC<AccordionItemProps> = ({
72  index,
73  children,
74  className,
75}) => {
76  const context = useContext(AccordionContext);
77  if (!context) {
78    throw new Error("AccordionItem must be used within an Accordion");
79  }
80  const { activeIndex, toggleAccordion, itemVariant, borderVariant } = context;
81  const isActive = activeIndex === index;
82  const contentRef = useRef<HTMLDivElement>(null);
83  const buttonRef = useRef<HTMLButtonElement>(null);
84  const [height, setHeight] = useState<number | string>("auto");
85
86  useLayoutEffect(() => {
87    if (contentRef.current && buttonRef.current) {
88      const buttonHeight = buttonRef.current.offsetHeight;
89      setHeight(
90        isActive
91          ? contentRef.current.scrollHeight + buttonHeight
92          : buttonHeight,
93      );
94    }
95  }, [isActive]);
96
97  return (
98    <motion.div
99    className={cn(
100      "h-max w-full overflow-hidden bg-zinc-950",
101      { "border border-white/20": borderVariant },
102      { "rounded-lg": itemVariant === "rounded" },
103      { "rounded-none": itemVariant === "rectangle" },
104      { "rounded-sm": itemVariant === "default" },
105      className,
106    )}
107    
108      animate={{ height }}
109      transition={springConfig}
110    >
111      {React.Children.map(children, (child) => {
112        if (React.isValidElement(child)) {
113          if (child.type === AccordionButton) {
114            return React.cloneElement(
115              child as React.ReactElement<AccordionButtonProps>,
116              {
117                ref: buttonRef,
118                isActive: isActive,
119                onClick: () => toggleAccordion(index),
120              },
121            );
122          } else if (child.type === AccordionContent) {
123            return (
124              <div ref={contentRef} className={child.props.className}>
125                <AnimatePresence initial={false}>
126                  {isActive &&
127                    React.cloneElement(
128                      child as React.ReactElement<AccordionContentProps>,
129                      {
130                        key: "content",
131                      },
132                    )}
133                </AnimatePresence>
134              </div>
135            );
136          }
137        }
138        return child;
139      })}
140    </motion.div>
141  );
142};
143
144interface AccordionButtonProps {
145  isActive?: boolean;
146  onClick?: () => void;
147  className?: ClassNameValue;
148  children: React.ReactNode;
149  ref?: React.Ref<HTMLButtonElement>;
150}
151
152const AccordionButton = React.forwardRef<
153  HTMLButtonElement,
154  AccordionButtonProps
155>(({ isActive, onClick, className, children }, ref) => {
156  return (
157    <button
158      ref={ref}
159      onClick={onClick}
160      className={cn(
161        "flex w-full items-center justify-between p-5 text-left font-geist font-semibold text-white transition-colors duration-300 hover:underline focus:outline-none",
162        className,
163      )}
164    >
165      <span>{children}</span>
166      <motion.div
167        animate={{ rotate: isActive ? 180 : 0 }}
168        transition={springConfig}
169      >
170        <ChevronDown className="h-6 w-6" />
171      </motion.div>
172    </button>
173  );
174});
175
176AccordionButton.displayName = "AccordionButton";
177
178interface AccordionContentProps {
179  className?: string;
180  children: React.ReactNode;
181}
182
183const AccordionContent: React.FC<AccordionContentProps> = ({
184  className,
185  children,
186}) => {
187  return (
188    <motion.div
189      key="content"
190      initial={{ opacity: 0 }}
191      animate={{ opacity: 1 }}
192      exit={{ opacity: 0 }}
193      transition={{ duration: 0.3 }}
194      className={cn(
195        "h-full w-full text-wrap px-5 py-4 font-geist text-sm font-normal text-gray-200",
196        className,
197      )}
198    >
199      {children}
200    </motion.div>
201  );
202};
203
204export { Accordion, AccordionItem, AccordionButton, AccordionContent };

Props

Accordion

The main wrapper component for the accordion.

PropTypeDefaultDescription
childrenReactNode-The accordion items.
classNameClassNameValue-Additional CSS classes.
variant"default" | "rounded" | "rectangle"-The visual style of the accordion.
borderboolean-Whether to show a border around the accordion.

AccordionItem

Represents a single item in the accordion.

PropTypeDefaultDescription
indexnumber-The index of the item in the accordion.
childrenReactNode-The content of the item. Should include AccordionButton and AccordionContent.
classNameClassNameValue-Additional CSS classes.

AccordionButton

The clickable header of an accordion item.

PropTypeDefaultDescription
isActiveboolean-Whether the item is currently expanded.
onClick() => void-Function to call when the button is clicked.
classNameClassNameValue-Additional CSS classes.
childrenReactNode-The content of the button.

AccordionContent

The expandable content of an accordion item.

PropTypeDefaultDescription
classNamestring-Additional CSS classes.
childrenReactNode-The content to display when the item is expanded.

FlaminUI

© 2024 FlaminUI. All rights reserved.

Made with ❤️ by FlaminUI Team