Skip to main content

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 when several disclosures share selection rules.

Basic usage#

import { Collapsible } from 'moraine'

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

Playground#

@solidjs starred 3 repositories
@solidjs/router
Props
Slots

Anatomy#

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 for override precedence.

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.

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.

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#

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#

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#

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
data-closedSlot: collapsible, collapsible-trigger, collapsible-contentDescription: Present when disclosure or transition content is closed.
data-expandedSlot: collapsible, collapsible-trigger, collapsible-contentDescription: Present when the panel, accordion, or menu is expanded.
data-disabledSlot: collapsible-triggerDescription: Present when the component, slot, or item is disabled.

Props#

Collapsible#

Props for the Collapsible component. Renders a <div> element by default.

Prop

Trigger#

Renders a <button> element by default.

Prop

Content#

Renders a <div> element by default.

Prop