---
name: collapsible
type: core
domain: navigation
requires: [loke-ui]
description: >
  Single collapsible section with Collapsible/CollapsibleTrigger/CollapsibleContent.
  CSS variable animation via --loke-collapsible-content-height and
  --loke-collapsible-content-width (measured via ResizeObserver-style layout effect).
  forceMount for exit animations. Controlled via open/onOpenChange.
  For multiple coordinated sections, use Accordion (which wraps Collapsible internally).
---

# Collapsible

## Setup

A collapsible section with a trigger button and hidden content.

```tsx
import {
  Collapsible,
  CollapsibleTrigger,
  CollapsibleContent,
} from "@loke/ui/collapsible";

function FilterPanel() {
  return (
    <Collapsible defaultOpen>
      <CollapsibleTrigger>Advanced filters</CollapsibleTrigger>
      <CollapsibleContent>
        <div>
          <label>
            <input type="checkbox" /> Include archived
          </label>
          <label>
            <input type="checkbox" /> Show drafts
          </label>
        </div>
      </CollapsibleContent>
    </Collapsible>
  );
}
```

`CollapsibleTrigger` renders a `<button>` with `aria-expanded` and `aria-controls` wired automatically.

## Core Patterns

### CSS variable animation

`CollapsibleContent` measures the real content dimensions before each open/close transition and exposes them as CSS custom properties. Use `--loke-collapsible-content-height` for smooth height animations — never use a fixed `max-height`.

```css
.collapsible-content {
  overflow: hidden;
}

.collapsible-content[data-state="open"] {
  animation: collapsible-open 200ms ease-out;
}

.collapsible-content[data-state="closed"] {
  animation: collapsible-close 200ms ease-in;
}

@keyframes collapsible-open {
  from { height: 0; }
  to   { height: var(--loke-collapsible-content-height); }
}

@keyframes collapsible-close {
  from { height: var(--loke-collapsible-content-height); }
  to   { height: 0; }
}
```

`data-state` is `"open"` or `"closed"` on both `Collapsible` and `CollapsibleContent`.

```tsx
<Collapsible>
  <CollapsibleTrigger className="collapsible-trigger">
    Show more
  </CollapsibleTrigger>
  <CollapsibleContent className="collapsible-content">
    Additional content here.
  </CollapsibleContent>
</Collapsible>
```

Source: `src/components/collapsible/collapsible.tsx` — `CollapsibleContentImpl` measures `getBoundingClientRect()` and sets CSS variables in a layout effect.

### Exit animations with `forceMount`

By default `CollapsibleContent` unmounts when closed, cutting off any exit animation. Use `forceMount` to keep the content in the DOM through the close animation. The component tracks `isPresent` internally via `Presence`.

```tsx
<Collapsible>
  <CollapsibleTrigger>Toggle</CollapsibleTrigger>
  <CollapsibleContent forceMount className="collapsible-content">
    This content stays mounted for exit animations.
  </CollapsibleContent>
</Collapsible>
```

Source: `src/components/collapsible/collapsible.tsx` — `Presence` with `forceMount || context.open`.

### Controlled state

```tsx
const [open, setOpen] = useState(false);

<Collapsible open={open} onOpenChange={setOpen}>
  <div style={{ display: "flex", justifyContent: "space-between" }}>
    <span>Repositories</span>
    <CollapsibleTrigger>
      {open ? "Hide" : "Show"} more
    </CollapsibleTrigger>
  </div>
  <CollapsibleContent>
    <ul>
      <li>repo-one</li>
      <li>repo-two</li>
    </ul>
  </CollapsibleContent>
</Collapsible>
```

### Disabled state

```tsx
<Collapsible disabled>
  <CollapsibleTrigger>Locked section</CollapsibleTrigger>
  <CollapsibleContent>Cannot be toggled.</CollapsibleContent>
</Collapsible>
```

`data-disabled=""` is set on both `Collapsible` and `CollapsibleContent` when disabled.

## Common Mistakes

### Using Collapsible for multiple expandable sections — use Accordion instead

`Collapsible` manages a single open/closed state. If you need multiple expandable sections — especially with single-selection (one open at a time) — use `Accordion`. Accordion wraps Collapsible internally and adds coordinated state, keyboard navigation (Home/End/Arrow), and grouped ARIA semantics.

```tsx
// Wrong — manual coordination of multiple Collapsibles
<Collapsible open={openA} onOpenChange={setOpenA}>...</Collapsible>
<Collapsible open={openB} onOpenChange={setOpenB}>...</Collapsible>

// Correct — use Accordion for coordinated sections
import { Accordion, AccordionItem, AccordionHeader, AccordionTrigger, AccordionContent } from "@loke/ui/accordion";

<Accordion type="single" collapsible>
  <AccordionItem value="a">
    <AccordionHeader><AccordionTrigger>Section A</AccordionTrigger></AccordionHeader>
    <AccordionContent>Content A</AccordionContent>
  </AccordionItem>
  <AccordionItem value="b">
    <AccordionHeader><AccordionTrigger>Section B</AccordionTrigger></AccordionHeader>
    <AccordionContent>Content B</AccordionContent>
  </AccordionItem>
</Accordion>
```

Source: `src/components/accordion/accordion.tsx` — Accordion wraps Collapsible with coordinated state.

### Hardcoded `max-height` for animation — janky transitions

The component measures actual content height at runtime. A fixed `max-height` value won't match the content and produces uneven easing.

```css
/* Wrong */
.collapsible-content[data-state="open"] {
  max-height: 300px;
  transition: max-height 200ms;
}

/* Correct */
.collapsible-content[data-state="open"] {
  height: var(--loke-collapsible-content-height);
}
```

Source: `src/components/collapsible/collapsible.tsx` — `--loke-collapsible-content-height` set from `getBoundingClientRect().height`.

### Expecting open animation on initial render with `defaultOpen={true}`

`CollapsibleContent` suppresses mount animation via `isMountAnimationPreventedRef`. When the component first renders with `defaultOpen={true}`, content appears instantly without animating in — this is intentional to avoid animation on page load.

```tsx
// Content appears instantly on first render — this is expected
<Collapsible defaultOpen>
  <CollapsibleTrigger>Section</CollapsibleTrigger>
  <CollapsibleContent>Initially visible, no entry animation.</CollapsibleContent>
</Collapsible>
```

Source: `src/components/collapsible/collapsible.tsx` — `isMountAnimationPreventedRef` cleared after first `requestAnimationFrame`.

## Cross-references

- **Accordion** (`@loke/ui/accordion`) — wraps Collapsible; use for multiple coordinated sections
- **Choosing the Right Component** — Collapsible vs Accordion decision guidance
