Accordion

Expandable sections; single-open by default or multiple.

import { Accordion, AccordionItem }from "@/components/ui"

FAQ

Everything in the free tier plus priority support.

Full source

The complete component, read from src/components/ui/accordion.tsx.

accordion.tsx
"use client";

import { useState, type ReactNode } from "react";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";

export interface AccordionItem {
  id: string;
  title: ReactNode;
  content: ReactNode;
}

interface AccordionProps {
  items: AccordionItem[];

  multiple?: boolean;

  defaultOpen?: string[];
}

export function Accordion({ items, multiple = false, defaultOpen = [] }: AccordionProps) {
  const [open, setOpen] = useState<Set<string>>(new Set(defaultOpen));

  const toggle = (id: string) => {
    setOpen((prev) => {
      const next = new Set(prev);
      if (next.has(id)) {
        next.delete(id);
      } else {
        if (!multiple) next.clear();
        next.add(id);
      }
      return next;
    });
  };

  return (
    <div className="divide-y divide-border overflow-hidden rounded-xl border border-border bg-surface">
      {items.map((item) => {
        const isOpen = open.has(item.id);
        const regionId = `accordion-${item.id}`;
        return (
          <div key={item.id}>
            <button
              type="button"
              aria-expanded={isOpen}
              aria-controls={regionId}
              onClick={() => toggle(item.id)}
              className="flex w-full items-center justify-between gap-3 px-4 py-3 text-left text-sm font-medium text-text transition-colors hover:bg-bg-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent"
            >
              <span className="min-w-0">{item.title}</span>
              <ChevronDown
                size={16}
                className={cn("shrink-0 text-text-tertiary transition-transform duration-200", isOpen && "rotate-180")}
                aria-hidden
              />
            </button>
            {isOpen && (
              <div id={regionId} role="region" className="px-4 pb-4 text-sm leading-relaxed text-text-secondary">
                {item.content}
              </div>
            )}
          </div>
        );
      })}
    </div>
  );
}

Dependencies

To use Accordion you also need the following. Copy these into your project alongside it.

Recommended folder structure

project structure
src/components/ui/
  └─ accordion.tsx
src/lib/
  └─ utils.ts