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

children: ReactNodeTrigger, panel and (optional) indicator elements.
defaultOpen: booleanfalseInitial open state (uncontrolled). Default false. Ignored when open is provided.
disabled: booleanDisable the disclosure - the trigger stops toggling and gets data-disabled.
onOpenChange: (open: boolean) => voidFires whenever the open state should change (trigger click).
open: booleanControlled open state. When provided, internal state is bypassed.

CollapsibleTrigger

children*: ReactNodeSingle React element to use as the trigger. Must accept onClick (a <button> is ideal).

CollapsiblePanel

C extends ElementType = 'div'
as: ElementType'div'Polymorphic root element. Defaults to 'div'.
children: ReactNodePanel 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: stringExtra classes for the panel root.
keepMounted: booleanfalseKeep 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

C extends ElementType = 'span'
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: stringExtra classes for the indicator root.