Collapsible

An interactive component that expands and collapses a panel.

Native source
<div mlk-collapsible-root class="package-list">
  <div class="package-list__row package-list__row--featured">
    <span>@milkui/core/collapsible</span>
    <span>Native primitive</span>
  </div>
  <a mlk-collapsible-trigger draggable="false" href="#packages-content">Show packages</a>
  <div mlk-collapsible-content id="packages-content" hidden="until-found">
    <div class="package-list__row">
      <span>@milkui/react/collapsible</span>
      <span>React adapter</span>
    </div>
    <div class="package-list__row">
      <span>@milkui/core/primitive</span>
      <span>Shared lifecycle</span>
    </div>
  </div>
</div>

<script type="module">
  import { Root, Trigger, Content } from '@milkui/core/collapsible';
  Root.define(document);
  Trigger.define(document);
  Content.define(document);
</script>

Features

  • Fragment links work before JavaScript loads.
  • Controlled or uncontrolled open state.
  • Semantic elements with no shadow DOM.
  • React asChild composition, or native marker attributes.
  • ARIA, data attributes, and keyboard behavior from shared primitive logic.
  • CSS transitions.

Anatomy

Triggers link to content marked hidden="until-found" before JavaScript. Following a link reveals that panel without resetting others. Enhancement adds toggle and keyboard behavior.

Native anatomy
<div mlk-collapsible-root>
  <a mlk-collapsible-trigger></a>
  <div mlk-collapsible-content></div>
</div>

API reference

Root

PropTypeDefaultDescription
defaultOpenbooleanfalseInitial uncontrolled open state.
openbooleanundefinedControlled open state.
onOpenChange(open: boolean) => voidundefinedState change request callback.
disabledbooleanfalsePrevents trigger activation.

Trigger

PropTypeDefaultDescription
disabledbooleanfalseDisables only this trigger.
hrefstringContent fragmentLink to the content before enhancement.

Content

No component-specific props.

Data attributeValues
[data-open]Present when open; absent when closed
[data-disabled]Present when disabled
[mlk-collapsible-root]Root marker
[mlk-collapsible-trigger]Trigger marker
[mlk-collapsible-content]Content marker
CSS variableDescription
--mlk-collapsible-content-widthMeasured content width.
--mlk-collapsible-content-heightMeasured content height.

Examples

Controlled

The primitive requests changes. Your state decides whether to accept them.

Native source
<script type="module">
  import { Root, Trigger, Content } from '@milkui/core/collapsible';
  Root.define(document);
  Trigger.define(document);
  Content.define(document);

  await Promise.resolve();
  let open = Root.get(document.getElementById('controlled-root')).open;
  const root = Root.mount(document.getElementById('controlled-root'), {
    open,
    onOpenChange(nextOpen: boolean) {
      if (accept.checked) setOpen(nextOpen);
    },
  });

  function setOpen(nextOpen: boolean) {
    open = nextOpen;
    root.update({ open });
  }
</script>

CSS transitions

Content stays mounted; CSS controls opening and closing motion.

Native source
<div mlk-collapsible-root id="animation-details">
  <a mlk-collapsible-trigger draggable="false" href="#animation-details-content">Animation details</a>
  <div mlk-collapsible-content id="animation-details-content" hidden="until-found">
    <div class="demo__content-inner">Animated content</div>
  </div>
</div>

<script type="module">
  import { Root, Trigger, Content } from '@milkui/core/collapsible';
  Root.define(document);
  Trigger.define(document);
  Content.define(document);
</script>
Transition styles
.package-list {
  width: min(100%, 520px);
  margin: 0 auto;
  padding: 18px;
  border: 1px solid rgba(124, 58, 237, 0.16);
  border-radius: var(--milk-radius, 14px);
  background: rgba(255, 255, 255, 0.92);
}

.package-list__row {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 4px 18px;
  padding: 13px 2px;
  border-top: 1px solid var(--milk-line, #e8e1f2);
  color: var(--milk-muted, #6d647d);
  font-size: 14px;
}

.package-list__row--featured {
  border-top: 0;
  color: var(--milk-text, #241d33);
  font-weight: 650;
}

[mlk-collapsible-content] > .package-list__row:first-child {
  border-top: 0;
}

.package-list__row span:last-child {
  color: var(--milk-muted, #6d647d);
  font-weight: 400;
}

[mlk-collapsible-trigger] {
  text-decoration: none;
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-top: 8px;
  padding: 12px 14px;
  border: 1px solid #d8cef4;
  border-radius: 10px;
  color: var(--milk-text, #241d33);
  background: white;
  text-align: left;
  cursor: pointer;
}

[mlk-collapsible-trigger]:hover:not(:disabled) {
  background: #faf7ff;
  text-decoration: none;
}

[mlk-collapsible-trigger]::after {
  content: '+';
  color: var(--milk-violet, #7c3aed);
  font-size: 20px;
  line-height: 1;
}

[mlk-collapsible-trigger][data-open]::after {
  content: '−';
}

[mlk-collapsible-trigger]:disabled,
[mlk-collapsible-trigger][aria-disabled='true'] {
  cursor: not-allowed;
  opacity: 0.55;
}

[mlk-collapsible-content] > .demo__content-inner {
  padding: 16px 2px 4px;
  color: #564d66;
  font-size: 14px;
  line-height: 1.65;
}

[mlk-collapsible-content] {
  scroll-margin-top: 4rem;
  height: 0;
  overflow: hidden;
  content-visibility: hidden;
  interpolate-size: allow-keywords;
  transition:
    height 220ms ease-in-out,
    content-visibility 220ms allow-discrete;
}

[mlk-collapsible-content][data-open] {
  height: auto;
  content-visibility: visible;
}

/* Enable transitions once the primitive is interactive. */
:not([data-interactive]) > [mlk-collapsible-content] {
  transition: none;
}

/* Before initialization, the browser reveals fragment targets by removing hidden. */
:not([data-interactive]) > [mlk-collapsible-content]:not([hidden]) {
  height: auto;
  content-visibility: visible;
}

:not([data-interactive]) > [mlk-collapsible-trigger]:has(~ [mlk-collapsible-content]:not([hidden]))::after {
  content: '−';
}

Nested composition

Nested roots resolve their nearest provider and stay isolated.

Native source
<div mlk-collapsible-root id="outer-details">
  <a mlk-collapsible-trigger draggable="false" href="#outer-details-content">Outer details</a>
  <div mlk-collapsible-content id="outer-details-content" hidden="until-found">
    <div mlk-collapsible-root id="inner-details">
      <a mlk-collapsible-trigger draggable="false" href="#inner-details-content">Inner details</a>
      <div mlk-collapsible-content id="inner-details-content" hidden="until-found">Inner state is isolated.</div>
    </div>
  </div>
</div>

Keyboard interactions

Before enhancement, triggers are links to sections. Once enhanced, Enter and Space toggle them without changing the URL.

KeyDescription
SpaceOpens or closes the collapsible.
EnterOpens or closes the collapsible.
TabMoves focus to the next focusable element.