Accordion

A simple accordion component

Accordion - Single Open

Loading...

Accordion - Multiple Open

Loading...

Accordion - Default Open

Loading...

Accordion - Custom Icons

Loading...

Usage Guide

Copy and paste the following component source.

"use client";

import { cn } from "@/lib/utils";
import { ChevronDown } from "lucide-react";
import { motion, AnimatePresence, Variants } from "framer-motion";
import React, { createContext, useContext, useState } from "react";

// Context
interface AccordionContextType {
    openItems: string[];
    toggleItem: (id: string) => void;
}

const AccordionContext = createContext<AccordionContextType | undefined>(undefined);

// Variants
const defaultVariants: Variants = {
    hidden: {
        opacity: 0,
        height: 0,
        transition: { duration: 0.3, ease: [0.04, 0.62, 0.23, 0.98] }
    },
    visible: {
        opacity: 1,
        height: "auto",
        transition: { duration: 0.3, ease: [0.04, 0.62, 0.23, 0.98] }
    }
};

/* -------------------------------------------------------------------------------------------------
 * Accordion (provider) component
 * -------------------------------------------------------------------------------------------------
 */

interface AccordionProps {
    children: React.ReactNode;
    allowMultipleOpen?: boolean;
    defaultOpen?: string[];
    className?: string;
}

const Accordion: React.FC<AccordionProps> = ({
    children,
    allowMultipleOpen = false,
    defaultOpen = [],
    className = ""
}) => {
    const [openItems, setOpenItems] = useState<string[]>(defaultOpen);

    const toggleItem = (id: string) => {
        setOpenItems((prev) => {
            if (allowMultipleOpen) {
                return prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id];
            } else {
                return prev.includes(id) ? [] : [id];
            }
        });
    };

    return (
        <AccordionContext.Provider value={{ openItems, toggleItem }}>
            <div className={cn("space-y-2", className)}>{children}</div>
        </AccordionContext.Provider>
    );
};

/* -------------------------------------------------------------------------------------------------
 * AccordionItem component
 * -------------------------------------------------------------------------------------------------
 */
interface AccordionItemProps {
    children: React.ReactNode;
    id: string;
    className?: string;
}

const AccordionItemContext = createContext<{ id: string } | undefined>(undefined);

const AccordionItem: React.FC<AccordionItemProps> = ({ children, id, className = "" }) => {
    return (
        <AccordionItemContext.Provider value={{ id }}>
            <div className={cn("overflow-hidden", className)} data-state={id}>
                {children}
            </div>
        </AccordionItemContext.Provider>
    );
};

/* -------------------------------------------------------------------------------------------------
 * AccordionTrigger component
 * -------------------------------------------------------------------------------------------------
 */
interface AccordionTriggerProps {
    children: React.ReactNode;
    className?: string;
    icon?: React.ReactNode;
    customOpenIcon?: React.ReactNode;
    customClosedIcon?: React.ReactNode;
}

const AccordionTrigger: React.FC<AccordionTriggerProps> = ({
    children,
    className = "",
    customOpenIcon,
    customClosedIcon
}) => {
    const context = useContext(AccordionContext);
    if (!context) throw new Error("AccordionTrigger must be used within an Accordion");

    const { openItems, toggleItem } = context;
    const itemContext = useContext(AccordionItemContext);
    if (!itemContext) throw new Error("AccordionTrigger must be used within an AccordionItem");

    const { id } = itemContext;
    const isOpen = openItems.includes(id);

    const renderIcon = () => {
        if (isOpen && customOpenIcon) return customOpenIcon;
        if (!isOpen && customClosedIcon) return customClosedIcon;
        return <ChevronDown className="h-4 w-4 shrink-0" />;
    };

    return (
        <motion.button
            className={cn(
                "w-full py-2 border-b flex items-center justify-between text-left",
                "hover:underline",
                className
            )}
            onClick={() => toggleItem(id)}
            aria-expanded={isOpen}
        >
            <span className="font-medium text-md">{children}</span>
            <motion.div
                initial={false}
                animate={{ rotate: isOpen ? 180 : 0 }}
                transition={{ duration: 0.3 }}
            >
                {renderIcon()}
            </motion.div>
        </motion.button>
    );
};

/* -------------------------------------------------------------------------------------------------
 * AccordionContent component
 * -------------------------------------------------------------------------------------------------
 */
interface AccordionContentProps {
    children: React.ReactNode;
    className?: string;
    variants?: Variants;
}

const AccordionContent: React.FC<AccordionContentProps> = ({
    children,
    className = "",
    variants = defaultVariants
}) => {
    const context = useContext(AccordionContext);
    if (!context) throw new Error("AccordionContent must be used within an Accordion");

    const { openItems } = context;
    const itemContext = useContext(AccordionItemContext);
    if (!itemContext) throw new Error("AccordionContent must be used within an AccordionItem");

    const { id } = itemContext;
    const isOpen = openItems.includes(id);

    return (
        <AnimatePresence initial={false}>
            {isOpen && (
                <motion.div initial="hidden" animate="visible" exit="hidden" variants={variants}>
                    <div className={cn("py-2 text-gray-500", "dark:text-gray-400", className)}>
                        {children}
                    </div>
                </motion.div>
            )}
        </AnimatePresence>
    );
};

export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

Props - Accordion

PropTypeDescriptionDefault
childrenReactNodeThe content to be rendered inside the Accordion_
defaultOpen?arrayAn array of IDs of items that should be open by default.[]
allowMultipleOpen?booleanIf true, allows multiple items to be open at the same time.false
className?stringAdditional class names to style the Accordion._

Props - AccordionItem

PropTypeDescriptionDefault
childrenReactNodeThe content to be rendered inside the AccordionItem._
idstringA unique identifier for the AccordionItem.undefined
className?stringAdditional class names to style the AccordionItem._

Props - AccordionTrigger

PropTypeDescriptionDefault
childrenReactNodeThe content to be rendered inside the trigger button for the AccordionItem._
className?stringAdditional class names to style the AccordionTrigger._
customOpenIcon?ReactNodeA custom icon to display when the item is openundefined
customCloseIcon?ReactNodeA custom icon to display when the item is closedundefined

Props - AccordionContent

PropTypeDescriptionDefault
childrenReactNodeThe content to be revealed when the AccordionItem is expanded._
className?stringAdditional class names to style the AccordionContent._
variants?ObjectFramer Motion variants for animating the AccordionContent._