---
title: "Accordion"
description: "Stacked, collapsible sections for settings panels and grouped content."
links:
  doc: https://cladd.io/react/components/accordion/
  api: https://cladd.io/react/components/accordion/#api-reference
---

# Accordion

`Accordion` is a stack of collapsible sections where, by default, opening one closes the rest — the canonical shape for a settings sidebar, an inspector, or a grouped preferences panel where you want a lot on screen without it all being open at once. It's a thin coordinator on top of [`Collapsible`](/react/components/collapsible/): each `AccordionItem` publishes the very same disclosure context, so `AccordionTrigger`, `AccordionPanel`, and `AccordionIndicator` are literally the `Collapsible` parts wearing accordion names. The kit brings the open-state logic, the height animation, and the aria wiring; you bring the trigger markup.

```tsx
<Surface outline className="w-96 overflow-hidden rounded-2xl">
  <AccordionRoot defaultValue="appearance">
    <AccordionItem value="appearance">
      <AccordionTrigger>
        <AccordionRow label="Appearance" />
      </AccordionTrigger>
      <AccordionPanel>
        <div className="px-4 pb-4 text-cladd-fg-soft">
          Theme, accent color, and interface density. Changes apply to the
          whole workspace.
        </div>
      </AccordionPanel>
    </AccordionItem>
    <AccordionItem value="notifications">
      <AccordionTrigger>
        <AccordionRow label="Notifications" />
      </AccordionTrigger>
      <AccordionPanel>
        <div className="px-4 pb-4 text-cladd-fg-soft">
          Choose what reaches your inbox: mentions, weekly digests, and
          release notes.
        </div>
      </AccordionPanel>
    </AccordionItem>
    <AccordionItem value="advanced">
      <AccordionTrigger>
        <AccordionRow label="Advanced" />
      </AccordionTrigger>
      <AccordionPanel>
        <div className="px-4 pb-4 text-cladd-fg-soft">
          Experimental flags, developer tooling, and the data export
          endpoint.
        </div>
      </AccordionPanel>
    </AccordionItem>
  </AccordionRoot>
</Surface>
```

## Usage

```tsx
import {
  AccordionIndicator,
  AccordionItem,
  AccordionPanel,
  AccordionRoot,
  AccordionTrigger,
} from '@cladd-ui/react';

<AccordionRoot defaultValue="appearance">
  <AccordionItem value="appearance">
    <AccordionTrigger>
      <button type="button" className="flex w-full justify-between px-4 py-2">
        Appearance
        <AccordionIndicator className="transition-transform data-[open]:rotate-90">
          <ChevronIcon />
        </AccordionIndicator>
      </button>
    </AccordionTrigger>
    <AccordionPanel>
      <div className="px-4 pb-4">Theme, accent color, and density.</div>
    </AccordionPanel>
  </AccordionItem>
</AccordionRoot>;
```

`AccordionRoot` owns the open value(s) — uncontrolled via `defaultValue`, controlled via `value` / `onValueChange` — and the single-vs-`multiple` mode. It renders no DOM of its own. Each `AccordionItem` binds a `value` and derives its own open state from the root. `AccordionTrigger` **clones** its single child to attach the toggle `onClick` plus the aria wiring (`aria-expanded`, `aria-controls`, `id`) and a `data-open` attribute — so a plain `<button>`, a [`Button`](/react/components/button/), or a custom row all work. `AccordionPanel` animates its `height` between `0` and its measured natural height; put block padding on a **nested** element so the panel can collapse all the way to zero.

### The chevron

cladd ships no icons — `AccordionIndicator` is an empty, `aria-hidden` slot you drop your own glyph into. Because it carries `data-open`, rotating a static chevron is pure CSS (`data-[open]:rotate-90`). For a full glyph swap (plus ↔ minus, say), pass a render function instead: `{({ open }) => (open ? <MinusIcon /> : <PlusIcon />)}`. You can also skip the component entirely and rotate an icon off the trigger's own `data-open` — the cloned child carries it too.

## Examples

Each example below shares one small local helper, `AccordionRow`, so every `AccordionItem` stays a single line instead of repeating the full trigger markup. It's built on cladd's own [`Button`](/react/components/button/) — a transparent, full-width ghost — so the row picks up hover, active, and focus-ring states for free rather than hand-rolling them. Because `AccordionTrigger` clones its child to inject the toggle `onClick`, the aria wiring, and `data-open`, the row only has to spread those props (`...props`) straight onto the `Button`. That's the whole helper:

```tsx
// `ChevronIcon` is your own glyph — cladd ships none.
function AccordionRow({
  label,
  ...props
}: { label: string } & ComponentPropsWithoutRef<'button'>) {
  return (
    <Button
      variant="transparent"
      outline={false}
      {...props}
      className="w-full rounded-none aria-disabled:pointer-events-none aria-disabled:opacity-50"
      contentClassName="justify-between px-4"
    >
      <span>{label}</span>
      <AccordionIndicator className="text-cladd-fg-soft transition-transform data-[open]:rotate-90">
        <ChevronIcon />
      </AccordionIndicator>
    </Button>
  );
}
```

Forgetting that `{...props}` spread is the one easy mistake here — without it the cloned `onClick` never reaches the `Button` and nothing toggles.

### Single (default)

By default the accordion is single-open: opening one section collapses whatever was open, and re-toggling the open section closes it down to nothing. `defaultValue` sets which item starts expanded. This is the right mode for a settings panel where one focused section at a time keeps the screen legible.

```tsx
<Surface outline className="w-96 overflow-hidden rounded-2xl">
  <AccordionRoot defaultValue="appearance">
    <AccordionItem value="appearance">
      <AccordionTrigger>
        <AccordionRow label="Appearance" />
      </AccordionTrigger>
      <AccordionPanel>
        <div className="px-4 pb-4 text-cladd-fg-soft">
          Theme, accent color, and interface density. Changes apply to the
          whole workspace.
        </div>
      </AccordionPanel>
    </AccordionItem>
    <AccordionItem value="notifications">
      <AccordionTrigger>
        <AccordionRow label="Notifications" />
      </AccordionTrigger>
      <AccordionPanel>
        <div className="px-4 pb-4 text-cladd-fg-soft">
          Choose what reaches your inbox: mentions, weekly digests, and
          release notes.
        </div>
      </AccordionPanel>
    </AccordionItem>
    <AccordionItem value="advanced">
      <AccordionTrigger>
        <AccordionRow label="Advanced" />
      </AccordionTrigger>
      <AccordionPanel>
        <div className="px-4 pb-4 text-cladd-fg-soft">
          Experimental flags, developer tooling, and the data export
          endpoint.
        </div>
      </AccordionPanel>
    </AccordionItem>
  </AccordionRoot>
</Surface>
```

### Multiple

Pass `multiple` to let items open and close independently — nothing auto-collapses. The selection becomes a `string[]`, so a controlled `value` is an array and `onValueChange` receives the full array. This is the FAQ / reference shape, where a reader may want several sections expanded side by side.

```tsx
<Surface outline className="w-96 overflow-hidden rounded-2xl">
  <AccordionRoot
    multiple
    value={open}
    onValueChange={(v) => setOpen(v as string[])}
  >
    <AccordionItem value="shipping">
      <AccordionTrigger>
        <AccordionRow label="Shipping" />
      </AccordionTrigger>
      <AccordionPanel>
        <div className="px-4 pb-4 text-cladd-fg-soft">
          Ships in 2–3 business days. Free over $50.
        </div>
      </AccordionPanel>
    </AccordionItem>
    <AccordionItem value="returns">
      <AccordionTrigger>
        <AccordionRow label="Returns" />
      </AccordionTrigger>
      <AccordionPanel>
        <div className="px-4 pb-4 text-cladd-fg-soft">
          30-day window, no questions asked. We cover return postage.
        </div>
      </AccordionPanel>
    </AccordionItem>
    <AccordionItem value="warranty">
      <AccordionTrigger>
        <AccordionRow label="Warranty" />
      </AccordionTrigger>
      <AccordionPanel>
        <div className="px-4 pb-4 text-cladd-fg-soft">
          Two years against manufacturing defects, extendable to five.
        </div>
      </AccordionPanel>
    </AccordionItem>
  </AccordionRoot>
</Surface>
```

### Controlled

Drive the open value yourself with `value` / `onValueChange` when the open state is owned by your app — synced to a route, persisted, or read elsewhere. In single-open mode `onValueChange` hands you a single `string` (or `undefined` when everything is closed); in `multiple` mode it hands you the array.

```tsx
<div className="flex w-96 flex-col gap-2">
  <span className="text-cladd-fg-soft">
    Open section: <span className="text-cladd-fg">{open ?? 'none'}</span>
  </span>
  <Surface outline className="overflow-hidden rounded-2xl">
    <AccordionRoot
      value={open}
      onValueChange={(v) => setOpen(v as string | undefined)}
    >
      <AccordionItem value="account">
        <AccordionTrigger>
          <AccordionRow label="Account" />
        </AccordionTrigger>
        <AccordionPanel>
          <div className="px-4 pb-4 text-cladd-fg-soft">
            Profile, email address, and connected sign-in providers.
          </div>
        </AccordionPanel>
      </AccordionItem>
      <AccordionItem value="billing">
        <AccordionTrigger>
          <AccordionRow label="Billing" />
        </AccordionTrigger>
        <AccordionPanel>
          <div className="px-4 pb-4 text-cladd-fg-soft">
            Plan, payment method, and invoice history.
          </div>
        </AccordionPanel>
      </AccordionItem>
      <AccordionItem value="team">
        <AccordionTrigger>
          <AccordionRow label="Team" />
        </AccordionTrigger>
        <AccordionPanel>
          <div className="px-4 pb-4 text-cladd-fg-soft">
            Invite members, manage roles, and revoke access.
          </div>
        </AccordionPanel>
      </AccordionItem>
    </AccordionRoot>
  </Surface>
</div>
```

### Disabled

`disabled` on a single `AccordionItem` freezes just that item — its trigger stops toggling and reads as unavailable (a section gated behind a higher plan, say). `disabled` on `AccordionRoot` freezes every item at once. The disabled state flows through to the trigger as `aria-disabled` and `data-disabled`, so you can dim it in CSS.

```tsx
<Surface outline className="w-96 overflow-hidden rounded-2xl">
  <AccordionRoot defaultValue="general">
    <AccordionItem value="general">
      <AccordionTrigger>
        <AccordionRow label="General" />
      </AccordionTrigger>
      <AccordionPanel>
        <div className="px-4 pb-4 text-cladd-fg-soft">
          Workspace name, default language, and time zone.
        </div>
      </AccordionPanel>
    </AccordionItem>
    <AccordionItem value="integrations">
      <AccordionTrigger>
        <AccordionRow label="Integrations" />
      </AccordionTrigger>
      <AccordionPanel>
        <div className="px-4 pb-4 text-cladd-fg-soft">
          Slack, GitHub, and webhook connections.
        </div>
      </AccordionPanel>
    </AccordionItem>
    <AccordionItem value="enterprise" disabled>
      <AccordionTrigger>
        <AccordionRow label="Enterprise (Pro plan)" />
      </AccordionTrigger>
      <AccordionPanel>
        <div className="px-4 pb-4 text-cladd-fg-soft">
          SSO, audit logs, and custom retention policies.
        </div>
      </AccordionPanel>
    </AccordionItem>
  </AccordionRoot>
</Surface>
```

### Settings panel

The layout cladd was built for: a stack of sections whose panels hold real controls — an [`Input`](/react/components/input/), a [`List`](/react/components/list/) of [`Switch`](/react/components/switch/) rows — framed by a single [`Surface`](/react/components/surface/). Single-open keeps exactly one group expanded so a dense panel stays readable. Dense, but not crowded.

```tsx
<Surface outline className="w-96 overflow-hidden rounded-2xl">
  <AccordionRoot defaultValue="workspace">
    <AccordionItem value="workspace">
      <AccordionTrigger>
        <AccordionRow label="Workspace" />
      </AccordionTrigger>
      <AccordionPanel>
        <div className="flex flex-col gap-2 px-4 pb-4">
          <SectionTitle>Display name</SectionTitle>
          <Input value={name} onChange={setName} />
        </div>
      </AccordionPanel>
    </AccordionItem>
    <AccordionItem value="editor">
      <AccordionTrigger>
        <AccordionRow label="Editor" />
      </AccordionTrigger>
      <AccordionPanel>
        <div className="px-2 pb-2">
          <List>
            <ListButton
              as="label"
              after={
                <Switch
                  as="span"
                  checked={toggles.autosave}
                  onChange={() => set('autosave')}
                />
              }
            >
              Autosave drafts
            </ListButton>
            <ListButton
              as="label"
              after={
                <Switch
                  as="span"
                  checked={toggles.cursors}
                  onChange={() => set('cursors')}
                />
              }
            >
              Show collaborator cursors
            </ListButton>
            <ListButton
              as="label"
              after={
                <Switch
                  as="span"
                  checked={toggles.compact}
                  onChange={() => set('compact')}
                />
              }
            >
              Compact density
            </ListButton>
          </List>
        </div>
      </AccordionPanel>
    </AccordionItem>
    <AccordionItem value="notifications">
      <AccordionTrigger>
        <AccordionRow label="Notifications" />
      </AccordionTrigger>
      <AccordionPanel>
        <div className="px-2 pb-2">
          <List>
            <ListButton
              as="label"
              after={
                <Switch
                  as="span"
                  checked={toggles.mentions}
                  onChange={() => set('mentions')}
                />
              }
            >
              Mentions
            </ListButton>
            <ListButton
              as="label"
              after={
                <Switch
                  as="span"
                  checked={toggles.digest}
                  onChange={() => set('digest')}
                />
              }
            >
              Weekly digest
            </ListButton>
            <ListButton
              as="label"
              after={
                <Switch
                  as="span"
                  checked={toggles.releases}
                  onChange={() => set('releases')}
                />
              }
            >
              Release notes
            </ListButton>
          </List>
        </div>
      </AccordionPanel>
    </AccordionItem>
  </AccordionRoot>
</Surface>
```

### Playground

`multiple` and the accent `color` compose freely. Toggle `multiple` to switch between one-at-a-time and independent sections; the color token flows down to anything inside the panels that reads the surface accent.

```tsx
<Surface
  outline
  className={`cladd-color-${color} w-96 overflow-hidden rounded-2xl`}
>
  <AccordionRoot multiple={multiple} defaultValue="overview">
    <AccordionItem value="overview">
      <AccordionTrigger>
        <AccordionRow label="Overview" />
      </AccordionTrigger>
      <AccordionPanel>
        <div className="px-4 pb-4 text-cladd-primary">
          The accent token flows down to anything that reads the surface
          color.
        </div>
      </AccordionPanel>
    </AccordionItem>
    <AccordionItem value="details">
      <AccordionTrigger>
        <AccordionRow label="Details" />
      </AccordionTrigger>
      <AccordionPanel>
        <div className="px-4 pb-4 text-cladd-fg-soft">
          Toggle <span className="text-cladd-primary">multiple</span> to
          let sections stay open independently.
        </div>
      </AccordionPanel>
    </AccordionItem>
    <AccordionItem value="more">
      <AccordionTrigger>
        <AccordionRow label="More" />
      </AccordionTrigger>
      <AccordionPanel>
        <div className="px-4 pb-4 text-cladd-fg-soft">
          In single mode, opening this collapses the others.
        </div>
      </AccordionPanel>
    </AccordionItem>
  </AccordionRoot>
</Surface>
```

## API Reference

### AccordionRoot

Stateful, non-visual root for the set of sections. Owns the open value(s) and the single-vs-`multiple` mode, publishing them to its `AccordionItem`s through context. Renders no DOM of its own.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children?` | `ReactNode` | — | `AccordionItem`s. |
| `defaultValue?` | `string \| string[]` | — | Initial open item(s) (uncontrolled). Ignored when `value` is provided. |
| `disabled?` | `boolean` | — | Disable the whole accordion - every item's trigger stops toggling. |
| `multiple?` | `boolean` | `false` | Allow more than one item open at once. Selection becomes an array. Default `false`. |
| `onValueChange?` | `(value: string \| string[] \| undefined) => void` | — | Fires whenever the open item(s) change.<br>Receives a single value (or `undefined` when all closed) in single-open mode, or the full array in `multiple` mode. |
| `value?` | `string \| string[]` | — | Controlled open item(s). A single value in single-open mode, an array when `multiple`.<br>When provided, internal state is bypassed. |

### AccordionItem

One section within an `AccordionRoot`. Binds a `value`, derives its open state from the surrounding accordion, and republishes it as a [`Collapsible`](/react/components/collapsible/) context — so the disclosure parts below work unchanged inside it. Renders a single `<div>` wrapper carrying `data-open` / `data-disabled`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children?` | `ReactNode` | — | The item's trigger, panel and (optional) indicator. |
| `className?` | `string` | — | Extra classes for the item wrapper. |
| `disabled?` | `boolean` | — | Disable just this item. Combined with the accordion's own `disabled`. |
| `value` | `string` | — | Identifies this item - matched against the accordion's open value(s). |

### AccordionTrigger

An alias of [`CollapsibleTrigger`](/react/components/collapsible/). **Clones** its single child to attach the toggle `onClick`, the aria wiring (`aria-expanded`, `aria-controls`, `id`), and a `data-open` attribute. The child must accept `onClick` — a `<button>` is ideal. Composes with any existing `onClick` on the child, and no-ops while the item is `disabled`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | — | Single React element to use as the trigger. Must accept `onClick` (a `<button>` is ideal). |

### AccordionPanel

An alias of [`CollapsiblePanel`](/react/components/collapsible/). The content region — animates its `height` between `0` and its measured natural height, carries the `region` role and aria wiring back to its trigger, and honors `prefers-reduced-motion`. Put block padding on a nested element so it collapses fully.

**Generics:** `C extends ElementType = 'div'`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `as?` | `ElementType` | `'div'` | Polymorphic root element. Defaults to `'div'`. |
| `children?` | `ReactNode` | — | Panel content. Put block padding on a **nested** element - the panel itself animates its `height` to zero, so vertical padding on it would keep it from fully collapsing. |
| `className?` | `string` | — | Extra classes for the panel root. |
| `keepMounted?` | `boolean` | `false` | Keep the panel mounted (at `height: 0`) while collapsed instead of unmounting it - preserves internal state and scroll position.<br>Default `false`: the panel still animates closed, then unmounts once the animation finishes. |

### AccordionIndicator

An alias of [`CollapsibleIndicator`](/react/components/collapsible/). An optional, `aria-hidden` slot for the open/closed affordance. Pass static markup and rotate it off `data-open` in CSS, or pass a render function receiving `{ open, disabled }` to swap glyphs entirely. Brings no glyph of its own.

**Generics:** `C extends ElementType = 'span'`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `as?` | `ElementType` | `'span'` | Polymorphic root element. Defaults to `'span'`. |
| `children?` | `ReactNode \| ((state: CollapsibleIndicatorState) => ReactNode)` | — | Indicator content. Either static markup (rotate or restyle it off the `data-open` attribute in CSS), or a render function receiving `{ open, disabled }` so you can swap glyphs entirely (chevron, plus↔minus, a spinning `×`, whatever). |
| `className?` | `string` | — | Extra classes for the indicator root. |
