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 instead.
The trigger is your own markup: pass a single element (a
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.
Status, region, and last sync time live here. The panel animates its height open and closed and respects reduced-motion settings.
<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
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.
Open state lives in your component, so any control can drive it.
<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 }.
<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 or other
control inside the panel should preserve its value across toggles.
<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.
<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 within the outer panel.
General options live here.
<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.
<div className="flex w-72 flex-col">
<CollapsibleRoot disabled={false}>
<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={false}>
<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
| Name: 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
| Name: Type | Default | Description |
|---|---|---|
| children*: ReactNode | — | Single React element to use as the trigger. Must accept onClick (a <button> is ideal). |
CollapsiblePanel
| Name: 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.Default false: the panel still animates closed, then unmounts once the animation finishes. |
CollapsibleIndicator
| Name: 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. |