Accordion
DisclosureExpandable content sections with single or multi-open.
Preview
Usage
example.jsx
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "@/components/ui/accordion";
export default function Example() {
return <Accordion />;
}Source Code
Copy this file into components/ui/accordion.jsx in your project.
accordion.jsx
"use client";
import { forwardRef, createContext, useContext, useState } from "react";
import { cn } from "@/lib/utils";
const AccordionContext = createContext({ openItems: [], toggle: () => {}, type: "single" });
const Accordion = forwardRef(({ className, type = "single", defaultValue = [], children, ...props }, ref) => {
const [openItems, setOpenItems] = useState(Array.isArray(defaultValue) ? defaultValue : [defaultValue]);
const toggle = (value) => {
if (type === "single") {
setOpenItems((prev) => prev.includes(value) ? [] : [value]);
} else {
setOpenItems((prev) => prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value]);
}
};
return (
<AccordionContext.Provider value={{ openItems, toggle, type }}>
<div ref={ref} className={cn("divide-y rounded-md border", className)} {...props}>{children}</div>
</AccordionContext.Provider>
);
});
Accordion.displayName = "Accordion";
const AccordionItem = forwardRef(({ className, value, children, ...props }, ref) => {
const { openItems, toggle } = useContext(AccordionContext);
const isOpen = openItems.includes(value);
return (
<div ref={ref} className={cn(className)} {...props}>
{typeof children === "function" ? children({ isOpen, toggle: () => toggle(value) }) : children}
</div>
);
});
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = forwardRef(({ className, value, children, ...props }, ref) => {
const { openItems, toggle } = useContext(AccordionContext);
const isOpen = openItems.includes(value);
return (
<button ref={ref} onClick={() => toggle(value)}
className={cn("flex w-full items-center justify-between py-4 px-4 text-sm font-medium transition-all hover:bg-accent/50 [&[data-state=open]>svg]:rotate-180", className)}
data-state={isOpen ? "open" : "closed"}
{...props}
>
{children}
<svg className="h-4 w-4 shrink-0 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
</button>
);
});
AccordionTrigger.displayName = "AccordionTrigger";
const AccordionContent = forwardRef(({ className, value, children, ...props }, ref) => {
const { openItems } = useContext(AccordionContext);
const isOpen = openItems.includes(value);
if (!isOpen) return null;
return (
<div ref={ref} className={cn("overflow-hidden px-4 pb-4 pt-0 text-sm text-muted-foreground", className)} {...props}>
{children}
</div>
);
});
AccordionContent.displayName = "AccordionContent";
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
Quick Install
Make sure you have the cn() utility set up. It requires clsx and tailwind-merge.
npm install clsx tailwind-merge