Collapsible

An interactive component that expands and collapses a panel.

Show packages
React source
import * as Collapsible from '@milkui/react/collapsible';

export function Packages() {
  return (
    <Collapsible.Root className="package-list">
      <div className="package-list__row package-list__row--featured">
        <span>@milkui/react/collapsible</span>
        <span>React primitive</span>
      </div>
      <Collapsible.Trigger>Show packages</Collapsible.Trigger>
      <Collapsible.Content>
        <div className="package-list__row">
          <span>@milkui/react/collapsible</span>
          <span>React adapter</span>
        </div>
        <div className="package-list__row">
          <span>@milkui/core/primitive</span>
          <span>Shared lifecycle</span>
        </div>
      </Collapsible.Content>
    </Collapsible.Root>
  );
}

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.

React anatomy
import * as Collapsible from '@milkui/react/collapsible';

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

API reference

Collapsible.Root

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

Collapsible.Trigger

PropTypeDefaultDescription
disabledbooleanfalseDisables only this trigger.
hrefstringContent fragmentLink to the content before enhancement.
asChildbooleanfalseRender onto the child element.

Collapsible.Content

PropTypeDefaultDescription
asChildbooleanfalseRender onto the child element.
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.

Release notes
React source
const [open, setOpen] = React.useState(false);
const [acceptRequests, setAcceptRequests] = React.useState(true);

<Collapsible.Root
  id="release-notes"
  open={open}
  onOpenChange={(nextOpen: boolean) => {
    if (acceptRequests) setOpen(nextOpen);
  }}
>
  <Collapsible.Trigger>Release notes</Collapsible.Trigger>
  <Collapsible.Content>
    <div className="demo__content-inner">Controlled content</div>
  </Collapsible.Content>
</Collapsible.Root>

CSS transitions

Content stays mounted; CSS controls opening and closing motion.

Animation details
React source
<Collapsible.Root id="animation-details">
  <Collapsible.Trigger>Animation details</Collapsible.Trigger>
  <Collapsible.Content>
    <div className="demo__content-inner">Animated content</div>
  </Collapsible.Content>
</Collapsible.Root>
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.

Outer details
React source
<Collapsible.Root id="outer-details">
  <Collapsible.Trigger asChild>
    <a>Outer details</a>
  </Collapsible.Trigger>
  <Collapsible.Content>
    <Collapsible.Root id="inner-details">
      <Collapsible.Trigger>Inner details</Collapsible.Trigger>
      <Collapsible.Content>Inner state is isolated.</Collapsible.Content>
    </Collapsible.Root>
  </Collapsible.Content>
</Collapsible.Root>

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.