Collapsible

One disclosure, rendered as <details>.

import { Collapsible } from '@apostel/bedrock'
// or, to drive it from React: import { Collapsible } from '@apostel/bedrock/controlled'
Loading demo…
Sourcecollapsible.tsx
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>

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>.

For an open prop React drives, import from @apostel/bedrock/controlled. That direction only: <details> has no cancelable hook, so onOpenChange cannot decline a toggle there either. To stop one, preventDefault() the Trigger's click. Both are on controlled state.

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 it with scripting disabled and toggles it.

What is not here