Collapsible

One disclosure, rendered as <details>.

import { Collapsible } from '@apostel/bedrock'
// or, for a veto: import { Collapsible } from '@apostel/bedrock/controlled'
Loading demo…
Source
import { Collapsible } from '../../src/index'

/**
 * A single `<details>`. Unlike Dialog the root *is* the element, because
 * `<summary>` only works as a child of `<details>` — so there is nothing to
 * wire by id and `defaultOpen` is a plain attribute rather than an imperative
 * call on mount.
 */
export default function CollapsibleDemo() {
  return (
    <Collapsible.Root style={{ width: '100%', maxWidth: '28rem' }}>
      <Collapsible.Trigger>Delivery details</Collapsible.Trigger>
      <Collapsible.Content>
        Ships from Rotterdam. Tracked, and signed for over €50.
      </Collapsible.Content>
    </Collapsible.Root>
  )
}

Anatomy

<Collapsible.Root>
  <Collapsible.Trigger />
  <Collapsible.Content />
</Collapsible.Root>

Unlike Dialog, the root is the element. <summary> only works as a child of <details>, so there is nothing to wire by id and no invoker involved — which is also why defaultOpen is a plain attribute rather than an imperative call on mount. The disclosure is open in the HTML before any script runs.

Collapsible.Root

Renders <details data-bedrock-collapsible>.

prop type notes
defaultOpen boolean Read once; the attribute does the rest.
onOpenChange (open: boolean) => void Reports; cannot refuse.
asChild boolean The child must render a <details>.

open is read on the first render only. React writes a DOM property when the value it renders changes, so reading it once means the user's own toggling is never undone by an unrelated re-render.

Collapsible.Trigger

Renders <summary>. It carries the expanded state itself, so there is no aria-expanded to write and no button to keep in step.

Collapsible.Content

Renders a <div>. Children mount only while open, so a form inside a closed panel resets itself when it closes.

Server-rendered markup is the exception: content renders on the server and on the hydrating render that must match it. A page whose JavaScript never arrives still has a complete, working disclosure — there is a test that loads the page with scripting disabled and toggles it.

What is not here