# Shadcn Accordion

> Free shadcn/ui accordion component for React — Radix-powered collapsible sections. The FAQ accordion, multiple-open, leading icons, contained cards, a plus/minus toggle, and controlled state. The examples official shadcn never shipped.

Source: https://designrevision.com/components/accordion

The **Shadcn Accordion** is the collapsible-sections component — the canonical <a href="https://ui.shadcn.com/docs/components/accordion" rel="nofollow">shadcn/ui</a> accordion built on <a href="https://www.radix-ui.com/primitives/docs/components/accordion" rel="nofollow">Radix</a>. Its number-one job is the **FAQ**, and the examples below cover that plus multiple-open, icons, a contained-card style, a plus/minus toggle, and controlled state.

## Built on Radix

Accordion is a thin wrapper over <a href="https://www.radix-ui.com/primitives/docs/components/accordion" rel="nofollow">Radix Accordion</a> with four parts — `Accordion`, `AccordionItem`, `AccordionTrigger`, `AccordionContent`. Radix handles the <a href="https://www.w3.org/WAI/ARIA/apg/patterns/accordion/" rel="nofollow">WAI-ARIA accordion pattern</a>: keyboard navigation, focus, and the `--radix-accordion-content-height` measurement that drives the open/close animation. The chevron rotates on open via `[&[data-state=open]>svg]:rotate-180`.

## The FAQ accordion

The most common accordion is the **FAQ** — questions as triggers, answers as content, `type="single" collapsible` so one opens at a time and can close again:

```tsx
<Accordion type="single" collapsible>
  <AccordionItem value="refunds">
    <AccordionTrigger>What is your refund policy?</AccordionTrigger>
    <AccordionContent>30-day money-back guarantee…</AccordionContent>
  </AccordionItem>
</Accordion>
```

For search, pair it with **FAQPage structured data** (JSON-LD) so the Q&A can surface as rich results — this gallery emits that schema on every component page. The FAQ example is the markup.

## Single vs multiple

`type="single"` opens one item at a time (add `collapsible` to allow closing it); `type="multiple"` lets several stay open and makes `value` an **array**. Use single for FAQs where focus matters, multiple for reference content the user scans across (the Multiple open example).

## Styling

The default is flush rows with a bottom border. Restyle with classNames — no custom component:

- **Leading icons** — put an icon before the label inside `AccordionTrigger` (the Leading icons example).
- **Contained cards** — give `AccordionItem` `rounded-lg border px-4` and the `Accordion` `space-y-2` for separated cards (the Contained / card example).
- **Plus / minus** — compose `AccordionPrimitive.Trigger` directly and toggle a `PlusIcon` / `MinusIcon` with `group-data-[state]` (the Plus / minus example).

## Controlled & disabled

Use `defaultValue` for an uncontrolled start, or `value` + `onValueChange` to **control** which item is open from elsewhere (a stepper, a URL). Disable an item with the `disabled` prop on `AccordionItem`. The Controlled & disabled example drives the accordion from external buttons and disables one step.

## Animation

The expand/collapse uses the `accordion-down` and `accordion-up` keyframes, which animate to Radix's `--radix-accordion-content-height`. **These aren't core Tailwind** — they come from `tw-animate-css` (or `tailwindcss-animate`). Add `@import "tw-animate-css";` to your global CSS, or the panel snaps open with no transition.

## Tabs vs Accordion

Use an Accordion for **stacked, vertical** sections where several may be open — FAQs, settings, long forms — especially on narrow screens. Use [Tabs](/components/tabs) for **peer** sections shown one at a time in a fixed area. Accordions expand downward; tabs switch in place.

## Accessibility & theming

Radix gives the accordion full keyboard support — Enter/Space toggles, arrow keys move between triggers, Home/End jump to the ends — and wires the trigger to its panel with `aria-controls`. It reads your design tokens — `--border`, `--ring` (focus), `--muted-foreground` (the chevron) — so it follows your theme, including dark mode, with no overrides.

## Installation

### accordion

**Install:**

```bash
npx shadcn@latest add @designrevision/accordion
```

**Dependencies:** @radix-ui/react-accordion, utils

**Props**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| type * | "single" \| "multiple" | — | On Accordion. "single" opens one item at a time; "multiple" allows several. |
| collapsible | boolean | false | On Accordion (type="single") — lets the open item close again. |
| value / defaultValue | string \| string[] | — | The open item(s) — controlled (value + onValueChange) or uncontrolled (defaultValue). |
| disabled | boolean | false | On AccordionItem — prevents it from opening. |

`*` required.

**Usage & accessibility:** A Radix accordion — four parts (Accordion, AccordionItem, AccordionTrigger, AccordionContent), with a chevron that rotates on open. Set type="single" collapsible for an FAQ, or type="multiple" to allow several open. The open/close animation uses the accordion-down / accordion-up keyframes from tw-animate-css (or tailwindcss-animate) — without it the panel snaps open.

```tsx
// components/ui/accordion.tsx
"use client"

import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "lucide-react"

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

function Accordion({
  ...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
  return <AccordionPrimitive.Root data-slot="accordion" {...props} />
}

function AccordionItem({
  className,
  ...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
  return (
    <AccordionPrimitive.Item
      data-slot="accordion-item"
      className={cn("border-b last:border-b-0", className)}
      {...props}
    />
  )
}

function AccordionTrigger({
  className,
  children,
  ...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
  return (
    <AccordionPrimitive.Header className="flex">
      <AccordionPrimitive.Trigger
        data-slot="accordion-trigger"
        className={cn(
          "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
          className
        )}
        {...props}
      >
        {children}
        <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
      </AccordionPrimitive.Trigger>
    </AccordionPrimitive.Header>
  )
}

function AccordionContent({
  className,
  children,
  ...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
  return (
    <AccordionPrimitive.Content
      data-slot="accordion-content"
      className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
      {...props}
    >
      <div className={cn("pt-0 pb-4", className)}>{children}</div>
    </AccordionPrimitive.Content>
  )
}

export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
```

## Examples

### Accordion

The canonical single-open, collapsible accordion — one item expanded at a time.

**Install:**

```bash
npx shadcn@latest add @designrevision/accordion-demo-01
```

**Dependencies:** @designrevision/accordion

```tsx
// components/ui/accordion-demo-01.tsx
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

export default function AccordionDemo01() {
  return (
    <Accordion
      type="single"
      collapsible
      defaultValue="item-1"
      className="w-[420px]"
    >
      <AccordionItem value="item-1">
        <AccordionTrigger>Is it accessible?</AccordionTrigger>
        <AccordionContent>
          Yes. It adheres to the WAI-ARIA design pattern and is fully keyboard
          navigable.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="item-2">
        <AccordionTrigger>Is it styled?</AccordionTrigger>
        <AccordionContent>
          Yes. It comes with default styles that match your design tokens, and
          you can override them with a className.
        </AccordionContent>
      </AccordionItem>
      <AccordionItem value="item-3">
        <AccordionTrigger>Is it animated?</AccordionTrigger>
        <AccordionContent>
          Yes. It animates open and closed with the accordion-down /
          accordion-up keyframes.
        </AccordionContent>
      </AccordionItem>
    </Accordion>
  )
}
```

---

### FAQ

A frequently-asked-questions accordion — the most common use, and a natural pair for **FAQPage** schema.

**Install:**

```bash
npx shadcn@latest add @designrevision/accordion-faq-01
```

**Dependencies:** @designrevision/accordion

```tsx
// components/ui/accordion-faq-01.tsx
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

const faqs = [
  {
    q: "What is your refund policy?",
    a: "We offer a 30-day money-back guarantee — no questions asked. Email support and we'll process it within 48 hours.",
  },
  {
    q: "Can I change my plan later?",
    a: "Yes. Upgrade or downgrade at any time from your billing settings; changes are prorated automatically.",
  },
  {
    q: "Do you offer a free trial?",
    a: "Every paid plan includes a 14-day free trial. No credit card required to start.",
  },
  {
    q: "How do I cancel my subscription?",
    a: "Cancel anytime from Settings → Billing. Your plan stays active until the end of the current period.",
  },
]

export default function AccordionFaq01() {
  return (
    <Accordion type="single" collapsible className="w-[480px]">
      {faqs.map((faq, index) => (
        <AccordionItem key={index} value={`faq-${index}`}>
          <AccordionTrigger>{faq.q}</AccordionTrigger>
          <AccordionContent className="text-muted-foreground">
            {faq.a}
          </AccordionContent>
        </AccordionItem>
      ))}
    </Accordion>
  )
}
```

---

### Multiple open

`type="multiple"` lets several sections stay open at once, with two open by default.

**Install:**

```bash
npx shadcn@latest add @designrevision/accordion-multiple-01
```

**Dependencies:** @designrevision/accordion

```tsx
// components/ui/accordion-multiple-01.tsx
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

const sections = [
  { value: "shipping", title: "Shipping", body: "Free standard shipping on orders over $50. Express options at checkout." },
  { value: "returns", title: "Returns", body: "Return unworn items within 30 days for a full refund." },
  { value: "warranty", title: "Warranty", body: "All products carry a two-year limited warranty against defects." },
]

export default function AccordionMultiple01() {
  return (
    <Accordion
      type="multiple"
      defaultValue={["shipping", "returns"]}
      className="w-[460px]"
    >
      {sections.map((section) => (
        <AccordionItem key={section.value} value={section.value}>
          <AccordionTrigger>{section.title}</AccordionTrigger>
          <AccordionContent className="text-muted-foreground">
            {section.body}
          </AccordionContent>
        </AccordionItem>
      ))}
    </Accordion>
  )
}
```

---

### Leading icons

An icon before each trigger label — the help-center category pattern.

**Install:**

```bash
npx shadcn@latest add @designrevision/accordion-icons-01
```

**Dependencies:** lucide-react, @designrevision/accordion

```tsx
// components/ui/accordion-icons-01.tsx
import { CreditCardIcon, ShieldCheckIcon, TruckIcon } from "lucide-react"

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

const items = [
  { value: "shipping", icon: TruckIcon, title: "Shipping & delivery", body: "Orders ship within 1–2 business days with tracked delivery." },
  { value: "payment", icon: CreditCardIcon, title: "Payment methods", body: "We accept all major cards, Apple Pay, and PayPal." },
  { value: "security", icon: ShieldCheckIcon, title: "Security", body: "Every transaction is protected with 256-bit encryption." },
]

export default function AccordionIcons01() {
  return (
    <Accordion type="single" collapsible className="w-[460px]">
      {items.map((item) => (
        <AccordionItem key={item.value} value={item.value}>
          <AccordionTrigger>
            <span className="flex items-center gap-3">
              <item.icon className="text-muted-foreground size-4 shrink-0" />
              {item.title}
            </span>
          </AccordionTrigger>
          <AccordionContent className="text-muted-foreground pl-7">
            {item.body}
          </AccordionContent>
        </AccordionItem>
      ))}
    </Accordion>
  )
}
```

---

### Contained / card

Each item wrapped as a bordered card with spacing between, instead of the default flush rows.

**Install:**

```bash
npx shadcn@latest add @designrevision/accordion-card-01
```

**Dependencies:** @designrevision/accordion

```tsx
// components/ui/accordion-card-01.tsx
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"

const items = [
  { value: "a", title: "Getting started", body: "Install the CLI and run the init command to scaffold your project." },
  { value: "b", title: "Configuration", body: "Edit the config file to set your theme tokens and import aliases." },
  { value: "c", title: "Deployment", body: "Push to your provider and connect the repo — builds run automatically." },
]

export default function AccordionCard01() {
  return (
    <Accordion type="single" collapsible className="w-[460px] space-y-2">
      {items.map((item) => (
        <AccordionItem
          key={item.value}
          value={item.value}
          className="bg-card rounded-lg border px-4 last:border-b"
        >
          <AccordionTrigger className="hover:no-underline">
            {item.title}
          </AccordionTrigger>
          <AccordionContent className="text-muted-foreground">
            {item.body}
          </AccordionContent>
        </AccordionItem>
      ))}
    </Accordion>
  )
}
```

---

### Plus / minus icon

A custom trigger that swaps the default chevron for a plus/minus that toggles via `group-data-[state]`.

**Install:**

```bash
npx shadcn@latest add @designrevision/accordion-plus-01
```

**Dependencies:** @radix-ui/react-accordion, lucide-react, @designrevision/accordion

```tsx
// components/ui/accordion-plus-01.tsx
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { MinusIcon, PlusIcon } from "lucide-react"

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

const items = [
  { value: "a", title: "Can I use this in a commercial project?", body: "Yes — it's free to use in personal and commercial projects." },
  { value: "b", title: "Do I need to credit you?", body: "No attribution is required, though it's always appreciated." },
  { value: "c", title: "Will there be updates?", body: "Yes. New components and examples ship regularly." },
]

export default function AccordionPlus01() {
  return (
    <Accordion type="single" collapsible className="w-[480px]">
      {items.map((item) => (
        <AccordionItem key={item.value} value={item.value}>
          <AccordionPrimitive.Header className="flex">
            {/* Custom trigger: swap the default chevron for a +/- toggle. */}
            <AccordionPrimitive.Trigger className="group focus-visible:ring-ring/50 flex flex-1 items-center justify-between gap-4 rounded-md py-4 text-left text-sm font-medium outline-none focus-visible:ring-[3px]">
              {item.title}
              <PlusIcon className="text-muted-foreground size-4 shrink-0 group-data-[state=open]:hidden" />
              <MinusIcon className="text-muted-foreground size-4 shrink-0 group-data-[state=closed]:hidden" />
            </AccordionPrimitive.Trigger>
          </AccordionPrimitive.Header>
          <AccordionContent className="text-muted-foreground">
            {item.body}
          </AccordionContent>
        </AccordionItem>
      ))}
    </Accordion>
  )
}
```

---

### Controlled & disabled

Drive the open item from external buttons via `value` / `onValueChange`, with one disabled step.

**Install:**

```bash
npx shadcn@latest add @designrevision/accordion-controlled-01
```

**Dependencies:** @designrevision/accordion, button

```tsx
// components/ui/accordion-controlled-01.tsx
"use client"

import * as React from "react"

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"
import { Button } from "@/components/ui/button"

const steps = [
  { value: "account", title: "Account", body: "Create your account and verify your email address." },
  { value: "profile", title: "Profile", body: "Add your name and a profile photo." },
  { value: "billing", title: "Billing", body: "Add a payment method — available once your profile is complete.", disabled: true },
]

export default function AccordionControlled01() {
  const [value, setValue] = React.useState("account")

  return (
    <div className="grid w-[460px] gap-3">
      <div className="flex items-center gap-2">
        {steps.map((step) => (
          <Button
            key={step.value}
            variant={value === step.value ? "default" : "outline"}
            size="sm"
            disabled={step.disabled}
            onClick={() => setValue(step.value)}
          >
            {step.title}
          </Button>
        ))}
      </div>
      <Accordion type="single" collapsible value={value} onValueChange={setValue}>
        {steps.map((step) => (
          <AccordionItem
            key={step.value}
            value={step.value}
            disabled={step.disabled}
          >
            <AccordionTrigger>{step.title}</AccordionTrigger>
            <AccordionContent className="text-muted-foreground">
              {step.body}
            </AccordionContent>
          </AccordionItem>
        ))}
      </Accordion>
    </div>
  )
}
```
