Accordion

Disclosure

Expandable 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