Accordion

Collapsible content panels — supports single or multiple open items, with animated chevron indicators and smooth expand/collapse transitions.

Preview

Basic accordion

import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/registry/ui-kit/tailwind-accordion/react'

function Example() {
  return (
    <Accordion defaultIndex={0}>
      <AccordionItem>
        <AccordionTrigger>What is Tailwind CSS?</AccordionTrigger>
        <AccordionContent>A utility-first CSS framework for rapid UI development.</AccordionContent>
      </AccordionItem>
      <AccordionItem>
        <AccordionTrigger>Is it free?</AccordionTrigger>
        <AccordionContent>Yes, Tailwind CSS is open-source and MIT licensed.</AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}

Multiple open

import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/registry/ui-kit/tailwind-accordion/react'

function Example() {
  return (
    <Accordion multiple defaultIndex={[0, 1]}>
      <AccordionItem>
        <AccordionTrigger>First item</AccordionTrigger>
        <AccordionContent>Content for the first item.</AccordionContent>
      </AccordionItem>
      <AccordionItem>
        <AccordionTrigger>Second item</AccordionTrigger>
        <AccordionContent>Content for the second item.</AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}

Component API

PropDefaultDescription
multiplefalseAllow multiple items to be open at the same time.
defaultIndexIndex or indices of items to open by default.
className""Additional CSS classes for the accordion wrapper.
AccordionItemWrapper for each collapsible panel.
AccordionTriggerClickable button that toggles the panel open/closed.
AccordionContentCollapsible content area revealed when the item is open.

Source Code

"use client";

import { createContext, useContext, useState, useCallback } from "react";

/* ── Context ── */

interface AccordionContextValue {
  openIndices: number[];
  toggle: (index: number) => void;
}

const AccordionContext = createContext<AccordionContextValue>({
  openIndices: [],
  toggle: () => {},
});

interface AccordionItemContextValue {
  index: number;
  isOpen: boolean;
  toggle: () => void;
}

const AccordionItemContext = createContext<AccordionItemContextValue>({
  index: 0,
  isOpen: false,
  toggle: () => {},
});

/* ── Accordion ── */

interface AccordionProps {
  multiple?: boolean;
  defaultIndex?: number | number[];
  className?: string;
  children: React.ReactNode;
}

function Accordion({ multiple = false, defaultIndex, className = "", children }: AccordionProps) {
  const [openIndices, setOpenIndices] = useState<number[]>(() => {
    if (defaultIndex === undefined) return [];
    return Array.isArray(defaultIndex) ? defaultIndex : [defaultIndex];
  });

  const toggle = useCallback(
    (index: number) => {
      setOpenIndices((prev) => {
        if (prev.includes(index)) {
          return prev.filter((i) => i !== index);
        }
        return multiple ? [...prev, index] : [index];
      });
    },
    [multiple]
  );

  return (
    <AccordionContext.Provider value={{ openIndices, toggle }}>
      <div
        className={`divide-y divide-zinc-200 rounded-lg border border-zinc-200 dark:divide-zinc-800 dark:border-zinc-800 ${className}`}
      >
        {Array.isArray(children)
          ? children.map((child, i) => (
              <AccordionItemContext.Provider
                key={i}
                value={{ index: i, isOpen: openIndices.includes(i), toggle: () => toggle(i) }}
              >
                {child}
              </AccordionItemContext.Provider>
            ))
          : children}
      </div>
    </AccordionContext.Provider>
  );
}

/* ── AccordionItem ── */

interface AccordionItemProps {
  className?: string;
  children: React.ReactNode;
}

function AccordionItem({ className = "", children }: AccordionItemProps) {
  return <div className={className}>{children}</div>;
}

/* ── AccordionTrigger ── */

interface AccordionTriggerProps {
  className?: string;
  children: React.ReactNode;
}

function AccordionTrigger({ className = "", children }: AccordionTriggerProps) {
  const { isOpen, toggle } = useContext(AccordionItemContext);

  return (
    <button
      type="button"
      onClick={toggle}
      className={`flex w-full items-center justify-between px-4 py-3.5 text-left text-sm font-medium text-zinc-900 transition-colors hover:bg-zinc-50 dark:text-zinc-100 dark:hover:bg-zinc-800/50 ${className}`}
      aria-expanded={isOpen}
    >
      {children}
      <svg
        className={`h-4 w-4 shrink-0 text-zinc-500 transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`}
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        strokeWidth="2"
        strokeLinecap="round"
        strokeLinejoin="round"
      >
        <path d="m6 9 6 6 6-6" />
      </svg>
    </button>
  );
}

/* ── AccordionContent ── */

interface AccordionContentProps {
  className?: string;
  children: React.ReactNode;
}

function AccordionContent({ className = "", children }: AccordionContentProps) {
  const { isOpen } = useContext(AccordionItemContext);

  return (
    <div
      className={`overflow-hidden transition-all duration-200 ${
        isOpen ? "max-h-96 opacity-100" : "max-h-0 opacity-0"
      } ${className}`}
    >
      <div className="px-4 pb-3.5 text-sm text-zinc-600 dark:text-zinc-400">{children}</div>
    </div>
  );
}

export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
export default Accordion;