---
title: Collapsible
description: Disclose one content region from a caller-placed trigger.
package: moraine
version: 0.6.0
repository: https://github.com/subframe7536/moraine
---

# Collapsible

> Disclose one content region from a caller-placed trigger.

Use Collapsible for one disclosure whose trigger and content belong to the surrounding layout. Use [Accordion](https://moraine.subf.dev/components/accordion.md) when several disclosures share selection rules.

## Basic usage

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

export function Example() {
  return (
    <Collapsible>
      <Collapsible.Trigger>Details</Collapsible.Trigger>
      <Collapsible.Content>More information</Collapsible.Content>
    </Collapsible>
  )
}
```

## Anatomy

```text
Collapsible [component; slot=root]
├── Collapsible.Trigger [part; slot=trigger]
└── contentWrapper [internal]
    └── Collapsible.Content [part; slot=content]
```

## Usage

### Styling

Configure `root`, `trigger`, and `content` through `collapsible.base` in your Theme. The Content element exposes `data-expanded` and `data-closed` for state styling. Collapsible also renders an internal wrapper with `data-expanded`, `data-closed`, and `data-transition` for measurement, height animation, presence, and accessibility; you cannot configure that wrapper through Theme, `classes`, `styles`, or Content props.

Root `classes` and `styles` set defaults for this Collapsible. Use direct `class` and `style` on the root, Trigger, or Content to style those elements. Content's `ref` and `as` apply to the inner Content element. `CollapsibleT.Slot`, `CollapsibleT.Classes`, and `CollapsibleT.Styles` type the slot overrides. There are no visual variants. See [Styling](https://moraine.subf.dev/docs/customization.md#the-4-layer-override-hierarchy) for override precedence.

```tsx
import { Collapsible, MoraineProvider } from 'moraine'
import { defineTheme } from 'moraine/theme'

const theme = defineTheme({
  collapsible: {
    base: {
      root: 'w-full rounded-lg border border-border',
      trigger: 'w-full px-4 py-3 text-left font-medium text-blue-600',
      content: 'border-t border-border p-4 text-sm text-muted-foreground',
    },
  },
})

export function DesignStyling() {
  return (
    <MoraineProvider theme={theme}>
      <Collapsible defaultOpen transition classes={{ trigger: 'text-emerald-600' }}>
        <Collapsible.Trigger style={{ 'letter-spacing': '0.02em' }}>
          Project details
        </Collapsible.Trigger>
        <Collapsible.Content class="text-foreground">
          Theme slots provide defaults; instance and child styles refine this disclosure.
        </Collapsible.Content>
      </Collapsible>
    </MoraineProvider>
  )
}
```

### State ownership

Use `open` with `onOpenChange` for controlled state, or `defaultOpen` when the component owns its initial state. The trigger and content primitives may be composed into a custom layout.

```tsx
import { Button, Collapsible } from 'moraine'
import { createSignal } from 'solid-js'

export function StateOwnership() {
  const [open, setOpen] = createSignal(false)

  return (
    <div class="max-w-md w-full space-y-3">
      <Collapsible open={open()} onOpenChange={setOpen}>
        <Collapsible.Trigger as={Button} size="xs" variant="outline">
          {open() ? 'Hide advanced settings' : 'Show advanced settings'}
        </Collapsible.Trigger>
        <Collapsible.Content class="text-sm text-muted-foreground mt-2 p-3 b-1 b-border rounded-lg">
          Advanced configuration parameters and telemetry endpoints.
        </Collapsible.Content>
      </Collapsible>
      <div class="flex gap-2 items-center">
        <Button size="xs" variant="outline" onClick={() => setOpen((v) => !v)}>
          External toggle ({open() ? 'open' : 'closed'})
        </Button>
      </div>
    </div>
  )
}
```

### Content lifetime

The content shell stays mounted so the trigger's `aria-controls` points to a real element even while closed. `unmountOnHide` controls the user content inside it. Keep user content mounted when it holds state that should survive closing.

```tsx
import { Button, Collapsible, Input } from 'moraine'

export function ContentLifetime() {
  return (
    <div class="max-w-md w-full">
      <Collapsible unmountOnHide={false} defaultOpen>
        <Collapsible.Trigger as={Button} size="xs" variant="outline">
          Persistent filter fields
        </Collapsible.Trigger>
        <Collapsible.Content class="mt-2 p-3 b-1 b-border rounded-lg space-y-2">
          <p class="text-xs text-muted-foreground">
            Input state persists while closed when unmountOnHide is false.
          </p>
          <Input defaultValue="tag:moraine" placeholder="Search filter" />
        </Collapsible.Content>
      </Collapsible>
    </div>
  )
}
```

### Keyboard interaction

The trigger uses button semantics by default. A polymorphic trigger follows the keyboard semantics of its supplied element or component.

## Examples

### Uncontrolled state

```tsx
import { Card, Collapsible, Icon, Switch } from 'moraine'
import { createSignal } from 'solid-js'

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

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

      <div class="h-40">
        <Card class="rounded-lg">
          <Card.Body class="px-3 py-3">
            <Collapsible transition={transition()}>
              <Collapsible.Trigger class="group flex w-full items-center justify-between">
                <span>How do I reset my password?</span>
                <Icon
                  name="i-lucide-chevron-down"
                  aria-hidden="true"
                  class="group-data-expanded:rotate-180 transition-transform"
                />
              </Collapsible.Trigger>
              <Collapsible.Content>
                <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.Content>
            </Collapsible>
          </Card.Body>
        </Card>
      </div>
    </div>
  )
}
```

### Controlled state

```tsx
import { Button, Collapsible, Icon, Switch } from 'moraine'
import { createSignal } from 'solid-js'

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

  return (
    <div class="max-w-md w-full 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()}
          onCheckedChange={setLocked}
          checkedIcon="i-lucide-lock"
          uncheckedIcon="i-lucide-lock-open"
        />
      </div>

      <Collapsible
        open={open()}
        disabled={locked()}
        onOpenChange={setOpen}
        class="b-1 b-border rounded-lg w-full"
      >
        <Collapsible.Trigger class="group text-sm font-medium px-4 py-3 text-left flex w-full items-center justify-between data-disabled:opacity-60">
          <span>June invoice #INV-2048</span>
          <Icon
            name="i-lucide-chevron-down"
            aria-hidden="true"
            class="group-data-expanded:rotate-180 text-muted-foreground transition-transform"
          />
        </Collapsible.Trigger>
        <Collapsible.Content class="text-sm text-foreground px-4 pb-4">
          <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" />
              <span>Locked invoices cannot be expanded from the row trigger.</span>
            </div>
          </div>
        </Collapsible.Content>
      </Collapsible>
    </div>
  )
}
```

### Composable primitives

```tsx
import { Button, Card, Collapsible, Icon } from 'moraine'

export function Composable() {
  return (
    <div class="max-w-md w-full">
      <Card class="rounded-xl">
        <Card.Body class="p-4">
          <Collapsible transition>
            <div class="flex items-center justify-between">
              <span class="text-sm text-foreground font-semibold">
                @solidjs starred 3 repositories
              </span>
              <Collapsible.Trigger
                as={Button}
                variant="ghost"
                size="icon-sm"
                aria-label="Toggle repositories"
              >
                <Icon name="i-lucide-chevrons-up-down" class="size-4" />
              </Collapsible.Trigger>
            </div>

            <div class="text-sm font-mono mt-2 px-4 py-2 border border-border rounded-md">
              @solidjs/router
            </div>

            <Collapsible.Content class="pt-2 space-y-2">
              <div class="text-sm font-mono px-4 py-2 border border-border rounded-md">
                @solidjs/testing-library
              </div>
              <div class="text-sm font-mono px-4 py-2 border border-border rounded-md">
                subframe7536/moraine
              </div>
            </Collapsible.Content>
          </Collapsible>
        </Card.Body>
      </Card>
    </div>
  )
}
```

## Attributes

| Attributes | Slot | Description |
| --- | --- | --- |
| `data-closed` | `collapsible`, `collapsible-trigger`, `collapsible-content` | Present when disclosure or transition content is closed. |
| `data-expanded` | `collapsible`, `collapsible-trigger`, `collapsible-content` | Present when the panel, accordion, or menu is expanded. |
| `data-disabled` | `collapsible-trigger` | Present when the component, slot, or item is disabled. |

## Props

### Collapsible

Props for the Collapsible component.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| 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). |
| transition | boolean \| undefined | false | Whether to keep content mounted until its height transition completes. |
| unmountOnHide | boolean \| undefined | true | Whether to unmount collapsible content when closed. |
| children | JSX.Element \| undefined | — | Content to render inside the collapsible. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| classes | Classes \| undefined | — | Family slot class defaults for this instance. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
| styles | Styles \| undefined | — | Family slot style defaults for this instance. |

### Collapsible.Trigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| disabled | boolean \| undefined | — | Whether this trigger is disabled. |
| as | T \| undefined | 'button' | Element or component to render as. |
| children | JSX.Element \| undefined | — | Trigger label and visual content. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |

### Collapsible.Content

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| forceMount | boolean \| undefined | false | Force mounting the content in the DOM even when closed. |
| unmountOnHide | boolean \| undefined | true | Whether to unmount content when closed. |
| as | T \| undefined | 'div' | Element or component to render inner content as. |
| children | JSX.Element \| undefined | — | Content to render. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
