# FAQ Accordion

> FAQ accordion with smooth height animation, single or multiple open items.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/faq-accordion
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/faq-accordion.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/faq-accordion-demo.tsx"
import { FaqAccordion } from "@/components/faq-accordion"

const items = [
  {
    question: "Can I use the components in commercial projects?",
    answer: "Yes. Every component is MIT licensed, so you can use it in personal and commercial work without attribution.",
  },
  {
    question: "Do I need to install a package?",
    answer: "No. The shadcn CLI copies the source into your project, so you own the code and can change anything.",
  },
  {
    question: "Which frameworks are supported?",
    answer: "Any React setup with Tailwind CSS v4 works, including Next.js, Vite, Remix and Astro.",
  },
  {
    question: "How do I get updates?",
    answer: "Run the install command again and review the diff. Since the code lives in your repo, nothing changes without you.",
  },
]

export default function FaqAccordionDemo() {
  return (
    <div className="w-full max-w-xl">
      <h2 className="mb-6 text-center text-2xl font-semibold tracking-tight">Frequently asked questions</h2>
      <FaqAccordion items={items} />
    </div>
  )
}
```

## Source

```tsx title="components/faq-accordion.tsx"
"use client"

import * as React from "react"
import { Plus } from "lucide-react"

import { cn } from "@/lib/utils"

export type FaqItem = { question: string; answer: React.ReactNode }

export interface FaqAccordionProps extends React.ComponentProps<"div"> {
  items: FaqItem[]
  /** Allow several answers to be open at once */
  multiple?: boolean
  /** Index of the item open on first render, -1 for none */
  defaultOpen?: number
  /** "separated" draws a card per item, "divided" uses lines between items */
  variant?: "separated" | "divided"
}

export function FaqAccordion({
  items,
  multiple = false,
  defaultOpen = 0,
  variant = "separated",
  className,
  ...props
}: FaqAccordionProps) {
  const id = React.useId()
  const [open, setOpen] = React.useState<Set<number>>(() => new Set(defaultOpen >= 0 ? [defaultOpen] : []))

  const toggle = (index: number) =>
    setOpen((current) => {
      const next = new Set(multiple ? current : [])
      if (current.has(index)) next.delete(index)
      else next.add(index)
      return next
    })

  return (
    <div className={cn("flex w-full flex-col", variant === "separated" ? "gap-3" : "divide-y border-y", className)} {...props}>
      {items.map((item, index) => {
        const expanded = open.has(index)
        const buttonId = `${id}-q${index}`
        const panelId = `${id}-a${index}`
        return (
          <div
            key={item.question}
            className={cn(
              variant === "separated" && "rounded-xl border bg-card transition-shadow",
              variant === "separated" && expanded && "shadow-sm"
            )}
          >
            <h3>
              <button
                id={buttonId}
                type="button"
                aria-expanded={expanded}
                aria-controls={panelId}
                onClick={() => toggle(index)}
                className={cn(
                  "flex w-full items-center justify-between gap-4 py-4 text-left text-sm font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring sm:text-base",
                  variant === "separated" ? "rounded-xl px-5" : "px-1"
                )}
              >
                {item.question}
                <Plus
                  className={cn(
                    "size-4 shrink-0 text-muted-foreground transition-transform duration-300",
                    expanded && "rotate-45 text-foreground"
                  )}
                />
              </button>
            </h3>
            <div
              id={panelId}
              role="region"
              aria-labelledby={buttonId}
              inert={!expanded}
              className={cn(
                "grid transition-[grid-template-rows,opacity] duration-300 ease-[cubic-bezier(.2,.8,.2,1)]",
                expanded ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0"
              )}
            >
              <div className="overflow-hidden">
                <div className={cn("pb-4 text-sm leading-relaxed text-muted-foreground", variant === "separated" ? "px-5" : "px-1")}>
                  {item.answer}
                </div>
              </div>
            </div>
          </div>
        )
      })}
    </div>
  )
}
```
