---
title: "Collapsible"
description: "Show or hide a single section behind one trigger."
links:
  doc: https://cladd.io/react/components/collapsible/
  api: https://cladd.io/react/components/collapsible/#api-reference
---

# Collapsible

A single disclosure: one trigger that shows or hides one panel. Use it for
optional details, advanced settings, and inline reveals where there's just
one thing to toggle — for stacked sections that open one at a time, reach for
[`Accordion`](/react/components/accordion/) instead.

The trigger is your own markup: pass a single element (a
[`Button`](/react/components/button/), a row, anything) and cladd clones it to
attach the click handler, `aria-expanded`, `aria-controls`, and a `data-open`
attribute. The panel animates its height between `0` and `auto` and respects
`prefers-reduced-motion`.

```tsx
<div className="flex w-72 flex-col">
  <CollapsibleRoot defaultOpen>
    <CollapsibleTrigger>
      <Button
        className="group"
        variant="transparent"
        contentClassName="justify-between"
        size="lg"
      >
        Connection details
        <ChevronIcon className="text-cladd-fg-soft transition-transform duration-200 group-data-[open]:-rotate-180" />
      </Button>
    </CollapsibleTrigger>
    <CollapsiblePanel>
      <p className="px-2 pt-2 text-cladd-fg-soft">
        Status, region, and last sync time live here. The panel animates
        its height open and closed and respects reduced-motion settings.
      </p>
    </CollapsiblePanel>
  </CollapsibleRoot>
</div>
```

## Usage

```tsx
import {
  CollapsibleRoot,
  CollapsibleTrigger,
  CollapsiblePanel,
  Button,
} from '@cladd-ui/react';

export default function Example() {
  return (
    <CollapsibleRoot defaultOpen>
      <CollapsibleTrigger>
        <Button variant="transparent">Toggle section</Button>
      </CollapsibleTrigger>
      <CollapsiblePanel>Panel content.</CollapsiblePanel>
    </CollapsibleRoot>
  );
}
```

## Examples

### Controlled

Drive open state yourself with `open` and `onOpenChange` when the toggle
lives outside the trigger's own subtree — a button elsewhere on screen, a
keyboard shortcut, or an effect.

```tsx
<div className="flex w-72 flex-col gap-2">
  <Button size="sm" variant="solid" onClick={() => setOpen((v) => !v)}>
    {open ? 'Collapse' : 'Expand'} from outside
  </Button>
  <div className="flex flex-col">
    <CollapsibleRoot open={open} onOpenChange={setOpen}>
      <CollapsibleTrigger>
        <Button
          className="group"
          variant="transparent"
          contentClassName="justify-between"
          size="lg"
        >
          Advanced options
          <ChevronIcon className="text-cladd-fg-soft transition-transform duration-200 group-data-[open]:-rotate-180" />
        </Button>
      </CollapsibleTrigger>
      <CollapsiblePanel>
        <p className="px-2 pt-2 text-cladd-fg-soft">
          Open state lives in your component, so any control can drive it.
        </p>
      </CollapsiblePanel>
    </CollapsibleRoot>
  </div>
</div>
```

### Indicator

`CollapsibleIndicator` reads the open state from context, so you can swap a
plus and minus glyph (or any pair) without wiring up state of your own. Pass a
render function to receive `{ open, disabled }`.

```tsx
<div className="flex w-72 flex-col">
  <CollapsibleRoot>
    <CollapsibleTrigger>
      <Button
        variant="transparent"
        contentClassName="justify-between"
        size="lg"
      >
        Billing address
        <CollapsibleIndicator className="text-cladd-fg-soft">
          {({ open }) => (open ? <MinusIcon /> : <PlusIcon />)}
        </CollapsibleIndicator>
      </Button>
    </CollapsibleTrigger>
    <CollapsiblePanel>
      <p className="px-2 pt-2 text-cladd-fg-soft">
        The indicator receives the open and disabled state, so you can
        swap glyphs without tracking it yourself.
      </p>
    </CollapsiblePanel>
  </CollapsibleRoot>
</div>
```

### Keep mounted

By default the panel unmounts when collapsed. Set `keepMounted` to keep its
content in the DOM — useful when an [`Input`](/react/components/input/) or other
control inside the panel should preserve its value across toggles.

```tsx
<div className="flex w-72 flex-col">
  <CollapsibleRoot>
    <CollapsibleTrigger>
      <Button
        className="group"
        variant="transparent"
        contentClassName="justify-between"
        size="lg"
      >
        API key
        <ChevronIcon className="text-cladd-fg-soft transition-transform duration-200 group-data-[open]:-rotate-180" />
      </Button>
    </CollapsibleTrigger>
    <CollapsiblePanel keepMounted>
      <div className="pt-2">
        <Input placeholder="sk-..." defaultValue="sk-live-9f3c2a" />
      </div>
    </CollapsiblePanel>
  </CollapsibleRoot>
</div>
```

### Disabled

A disabled root makes the trigger inert: clicks are ignored and the panel
stays put. The trigger element also receives `disabled` and a `data-disabled`
attribute for styling.

```tsx
<div className="flex w-72 flex-col">
  <CollapsibleRoot disabled>
    <CollapsibleTrigger>
      <Button
        className="group"
        variant="transparent"
        contentClassName="justify-between"
        size="lg"
      >
        Locked section
        <ChevronIcon className="text-cladd-fg-soft transition-transform duration-200 group-data-[open]:-rotate-180" />
      </Button>
    </CollapsibleTrigger>
    <CollapsiblePanel>
      <p className="px-2 pt-2 text-cladd-fg-soft">
        A disabled root makes the trigger inert and ignores toggles.
      </p>
    </CollapsiblePanel>
  </CollapsibleRoot>
</div>
```

### Nested

Because each Collapsible is self-contained, you can nest one inside another
panel to build a dense disclosure tree — here an "Advanced" reveal lives
inside a [`Surface`](/react/components/surface/) within the outer panel.

```tsx
<div className="flex w-72 flex-col">
  <CollapsibleRoot defaultOpen>
    <CollapsibleTrigger>
      <Button
        className="group"
        variant="transparent"
        contentClassName="justify-between"
        size="lg"
      >
        Project settings
        <ChevronIcon className="text-cladd-fg-soft transition-transform duration-200 group-data-[open]:-rotate-180" />
      </Button>
    </CollapsibleTrigger>
    <CollapsiblePanel>
      <Surface
        level={2}
        outline
        className="mt-2 rounded-xl"
        contentClassName="flex flex-col gap-2 p-4"
      >
        <p className="text-cladd-fg-soft">General options live here.</p>
        <div className="flex flex-col">
          <CollapsibleRoot>
            <CollapsibleTrigger>
              <Button
                className="group"
                variant="transparent"
                contentClassName="justify-between"
                size="lg"
              >
                Advanced
                <ChevronIcon className="text-cladd-fg-soft transition-transform duration-200 group-data-[open]:-rotate-180" />
              </Button>
            </CollapsibleTrigger>
            <CollapsiblePanel>
              <p className="pt-1 text-cladd-fg-softer">
                Nest a collapsible inside a panel for a dense disclosure
                tree.
              </p>
            </CollapsiblePanel>
          </CollapsibleRoot>
        </div>
      </Surface>
    </CollapsiblePanel>
  </CollapsibleRoot>
</div>
```

### Playground

Toggle `keepMounted` and `disabled` to see how each affects the panel.

```tsx
<div className="flex w-72 flex-col">
  <CollapsibleRoot disabled={disabled}>
    <CollapsibleTrigger>
      <Button
        className="group"
        variant="transparent"
        contentClassName="justify-between"
        size="lg"
      >
        Notifications
        <ChevronIcon className="text-cladd-fg-soft transition-transform duration-200 group-data-[open]:-rotate-180" />
      </Button>
    </CollapsibleTrigger>
    <CollapsiblePanel keepMounted={keepMounted}>
      <p className="px-2 pt-2 text-cladd-fg-soft">
        Toggle the controls to see how keepMounted and disabled change the
        panel.
      </p>
    </CollapsiblePanel>
  </CollapsibleRoot>
</div>
```

## API Reference

### CollapsibleRoot

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children?` | `ReactNode` | — | Trigger, panel and (optional) indicator elements. |
| `defaultOpen?` | `boolean` | `false` | Initial open state (uncontrolled). Default `false`. Ignored when `open` is provided. |
| `disabled?` | `boolean` | — | Disable the disclosure - the trigger stops toggling and gets `data-disabled`. |
| `onOpenChange?` | `(open: boolean) => void` | — | Fires whenever the open state should change (trigger click). |
| `open?` | `boolean` | — | Controlled open state. When provided, internal state is bypassed. |

### CollapsibleTrigger

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

### CollapsiblePanel

**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. |

### CollapsibleIndicator

**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. |
