collapse
Animates a panel between zero and its natural height. The preset for accordions and collapsibles, where fading alone leaves the layout jumping. Takes a single element child.
Use it
html
data-motion="collapse"What it sets
The preset only declares these. Everything else, duration and easing included, comes from the shared tokens, so overriding a token restyles every preset at once rather than one of them.
css
[data-motion~="collapse"] {
/* The grid mechanism. A row track between `0fr` and `1fr` is two concrete flex
* values, so the enter and the exit both have a start value the browser can
* interpolate.
*
* Animating `height` cannot say that. The open steady state is `auto`, and
* `auto` to 0 is a discrete transition: under allow-discrete the panel hangs at
* full height for half the duration and then snaps shut. `interpolate-size:
* allow-keywords` fixed it by making `auto` interpolatable, and that property is
* Chromium-only, so every other engine got the snap. A track list interpolates
* in all three.
*
* Nothing here reads a host height variable any more. The four names Radix and
* Base UI publish between them are out of the mechanism entirely, so there is
* nothing left to resolve or to keep in step with either library.
*
* The cost is one composition rule: the panel takes a single element child, and
* that child is what gets clipped. Every accordion and collapsible page states
* it in its composition tree. */
grid-template-rows: 1fr;
overflow: hidden;
/* grid-template-rows is absent from the base transition list, where every
* property is compositor-friendly. Collapsing is a layout move, so it opts in
* here. */
transition-property: opacity, translate, scale, rotate, grid-template-rows, display, overlay, content-visibility;
/* Declared only while the panel is visible. `[hidden]` is a user-agent rule of
* `display: none` and any author `display` beats it, so an unconditional
* `display: grid` would stop [hidden] from ever hiding a plain panel. */
&:not([hidden]) {
display: grid;
}
/* A grid item's automatic minimum size is its content, so the track cannot
* shrink below it. The child's own overflow is what makes that minimum zero. */
& > * {
min-height: 0;
overflow: hidden;
}
/* Radix keyframe path. A second animation track carries the row size, so this
* still composes with fade. fill-mode `none` on enter hands the track back to
* the resting 1fr, letting the panel grow with its content after it has
* opened. */
&:is([data-state="open"], [data-state="visible"]):not([data-starting-style]):not([data-ending-style]) {
animation-name: motion-enter, motion-collapse-enter;
animation-fill-mode: both, none;
}
&:is([data-state="closed"], [data-state="hidden"]):not([data-starting-style]):not([data-ending-style]) {
animation-name: motion-exit, motion-collapse-exit;
}
/* Transition path: Base UI, and any plain [hidden] panel. The closed list is
* written last so it wins on the opening frame, where Base UI sets data-open
* and data-starting-style together and the panel must still start at zero. */
&:is([data-closed], [data-starting-style], [data-ending-style], [hidden], [data-motion-state="closed"]) {
grid-template-rows: 0fr;
}
}Composition
Composes with fade. Combine them with a space.
html
data-motion="collapse fade"Do not combine it with scale-fade, slide-up.
Hosts
The same attribute across every host it supports. You never choose the mechanism; the host's own state attributes do.
radix
<Accordion.Content data-motion="collapse" />
base-ui
<Accordion.Panel data-motion="collapse" />
plain
<div hidden data-motion="collapse" />