---
title: Collapsible
description: Expandable content section with optional height transitions.
sidebar:
  order: 8
search:
  tags: [disclosure, expand, collapse, transition]
---

# Collapsible

> Expandable content section with optional height transitions.

## Import

```tsx
import { Collapsible } from 'moraine'
```

## Slot Structure

Single expandable section with a trigger slot.

```text
root
├── trigger
└── content-wrapper
    └── content
```

## Examples

### Uncontrolled and Transition

FAQ pattern with optional height transition. Spacing belongs inside the content so the wrapper can collapse without layout shifts.

```tsx
function Uncontrolled() {
  const [transition, setTransition] = createSignal(true)

  return (
    <div class="max-w-xs w-full space-y-3">
      <Switch label="Transition" checked={transition()} onChange={setTransition} />

      <div class="h-40">
        <Card
          classes={{
            root: 'py-3 rounded-lg',
            body: 'px-3 mb-0',
          }}
        >
          <Collapsible
            transition={transition()}
            triggerRender={(context) => (
              <Button
                {...context.triggerProps}
                variant="ghost"
                class="text-sm w-full justify-between"
              >
                <span>How do I reset my password?</span>
                <Icon
                  name="i-lucide-chevron-down"
                  aria-hidden="true"
                  class={`text-muted-foreground shrink-0 size-4 transition-transform ${context.isOpen ? 'rotate-180' : ''}`}
                />
              </Button>
            )}
          >
            <div class="text-sm text-muted-foreground pt-3">
              You can reset your password from Account settings. We send a verification link to the
              primary email on the workspace.
            </div>
          </Collapsible>
        </Card>
      </div>
    </div>
  )
}
```

### Controlled and Disabled

External state controls a billing panel and can temporarily disable the trigger while an operation is locked.

```tsx
function Controlled() {
  const [open, setOpen] = createSignal(true)
  const [locked, setLocked] = createSignal(false)

  return (
    <div class="max-w-xl space-y-3">
      <div class="flex flex-wrap gap-2 items-center">
        <Button size="sm" onClick={() => !locked() && setOpen((value) => !value)}>
          {open() ? 'Hide invoice details' : 'Show invoice details'}
        </Button>
        <Switch
          label="Lock edits"
          checked={locked()}
          onChange={setLocked}
          checkedIcon="i-lucide-lock"
          uncheckedIcon="i-lucide-lock-open"
        />
      </div>

      <Collapsible
        open={open()}
        disabled={locked()}
        onOpenChange={setOpen}
        classes={{
          root: 'w-108 rounded-lg b-(1 border)',
          trigger:
            'w-full px-4 py-3 text-left text-sm font-medium flex items-center justify-between data-disabled:opacity-60',
          content: 'px-4 pb-4 text-sm text-foreground',
        }}
        triggerRender={(context) => (
          <button {...context.triggerProps} class="flex w-full items-center justify-between">
            <span>June invoice #INV-2048</span>
            <Icon
              name="i-lucide-chevron-down"
              aria-label="Toggle invoice details"
              class={cn('text-muted-foreground transition-transform', open() ? 'rotate-180' : '')}
            />
          </button>
        )}
      >
        <div class="space-y-2">
          <div class="flex items-center justify-between">
            <span class="text-muted-foreground">Plan</span>
            <span>Team Pro</span>
          </div>
          <div class="flex items-center justify-between">
            <span class="text-muted-foreground">Seats</span>
            <span>18 active</span>
          </div>
          <div class="text-muted-foreground flex gap-2 items-center">
            <Icon name="i-lucide-info" class="size-4" />
            <span>Locked invoices cannot be expanded from the row trigger.</span>
          </div>
        </div>
      </Collapsible>
    </div>
  )
}
```

### Stateful Trigger

Compose a compact row where only the icon button toggles detail content.

```tsx
function Stateful() {
  return (
    <div class="max-w-xl space-y-2">
      <Collapsible
        defaultOpen
        classes={{
          root: 'rounded-lg b-(1 border)',
          trigger: 'w-xl px-3 py-2 text-left text-sm flex items-center justify-between',
          content: 'px-3 pb-3 text-sm text-foreground',
        }}
        triggerRender={(context) => (
          <>
            <div class="min-w-0">
              <div class="flex gap-2 items-center">
                <Icon name="i-lucide-rocket" class="text-muted-foreground shrink-0 size-4" />
                <span class="font-medium truncate">Production deploy #4812</span>
              </div>
              <p class="text-xs text-muted-foreground mt-0.5">Completed 6 minutes ago</p>
            </div>
            <Button
              {...context.triggerProps}
              aria-label="Toggle deploy details"
              size="sm"
              variant="secondary"
              classes={{
                label: `transition-transform ${context.isOpen ? 'rotate-180' : ''}`,
              }}
            >
              <Icon name="i-lucide-chevron-down" />
            </Button>
          </>
        )}
      >
        <div class="space-y-1.5">
          <p>Commit b8a29c7 promoted the billing workspace and search index workers.</p>
          <p class="text-muted-foreground">Runtime: 2m 14s. Region: iad1. Rollback available.</p>
        </div>
      </Collapsible>
    </div>
  )
}
```

## API Reference

### Attributes

#### `root`

Container that owns the trigger and expandable content state.

#### `trigger`

Button users activate to toggle the content visibility.

#### `content`

Region that is mounted for the expanded collapsible content.

### Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| children | JSX.Element | — | Content to render inside the collapsible. |
| class | ClassValue | — | Class applied to the component root or trigger element. |
| classes | CollapsibleT.Classes \| undefined | — | — |
| defaultOpen | boolean \| undefined | false | Whether the collapsible is open by default (uncontrolled). |
| disabled | boolean \| undefined | false | Whether the collapsible is disabled. |
| id | string \| undefined | — | Unique identifier for the collapsible root element. |
| onOpenChange | ((open: boolean) => void) \| undefined | — | Callback when the open state changes. |
| open | boolean \| undefined | — | Whether the collapsible is open (controlled). |
| ref | JSX.HTMLElementTags["div"] extends { ref?: infer Ref; } ? Ref : never \| undefined | — | — |
| style | JSX.CSSProperties \| undefined | — | — |
| styles | CollapsibleT.Styles \| undefined | — | — |
| transition | boolean \| undefined | false | Whether to keep content mounted until its height transition completes. |
| triggerRender* | ComponentOrElement<CollapsibleT.TriggerRenderProps> | — | Custom trigger renderer. Spread `triggerProps` onto the interactive trigger element. |

### ARIA

Accessibility attributes and roles emitted by the component markup.

| Attribute | Type | Description |
| --- | --- | --- |
| aria-labelledby | boolean \| string \| undefined | References the element that labels the control or region. |

### Data Attributes

State and slot attributes exposed for styling hooks and selectors.

| Attribute | Type | Description |
| --- | --- | --- |
| data-slot | string | Identifies the rendered slot for styling hooks and selectors. |
