Animating to height: auto With interpolate-size and calc-size()
"Transition height to auto" is one of the oldest unsolved requests in CSS. Accordions, expanding cards, "show more" sections and collapsible navigation all want to grow to fit their content, but auto is a keyword, and the browser has never interpolated between a length and a keyword — so height: 0 to height: auto simply snapped. The workarounds were all compromises: animating max-height to a guessed large value, measuring in JavaScript, or scaling with transforms and distorting the content. The narrow problem this page solves is animating to and from intrinsic sizes properly, using interpolate-size where it exists and a robust grid fallback where it does not. It belongs to Dialog & Popover Animations in the CSS-Only Micro-Interactions & Animations guide.
Why the old workarounds were not good enough
The max-height trick sets max-height: 0 when closed and something like max-height: 1000px when open, and transitions max-height. It works, but the timing is wrong: the transition runs across 1000 pixels, so for a 200-pixel panel the visible part of the animation happens in the first fifth of the duration when opening and the last fifth when closing. Opening looks too fast, closing appears to hang and then snap. And if content ever exceeds the guess, it is clipped.
Measuring with JavaScript — reading scrollHeight and setting it as an inline height — gets the timing right but costs a forced layout per toggle and puts the animation in script. Scaling with transform: scaleY() is compositor-friendly but squashes text and does not move the content below.
What was needed was for the browser to resolve auto to its actual pixel size for the purpose of interpolation. That is exactly what interpolate-size does.
The complete implementation
The accordion below uses the native <details> element, ::details-content for the animatable panel, and interpolate-size to reach the content's real height. It needs no script at all.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Animating to height: auto</title>
<style>
/* Opt in once. The property inherits, so every descendant may now
interpolate to and from auto, fit-content, min-content, etc. */
:root { interpolate-size: allow-keywords; }
body { font: 16px/1.6 system-ui, sans-serif; margin: 2rem; max-width: 36rem; }
details {
border: 1px solid #cbd5e1;
border-radius: 10px;
margin-block-end: 0.75rem;
}
summary {
padding: 0.75rem 1rem;
cursor: pointer;
font-weight: 600;
}
summary:focus-visible { outline: 2px solid #4f46e5; outline-offset: 2px; border-radius: 10px; }
/* ::details-content is the wrapper around everything except <summary>.
Closed: zero height, hidden overflow. content-visibility is what the
browser toggles to hide it, so it must be allowed to transition too. */
details::details-content {
block-size: 0;
overflow: clip;
transition:
block-size 300ms cubic-bezier(0.22, 1, 0.36, 1),
content-visibility 300ms allow-discrete;
}
/* Open: grow to the content's natural height. */
details[open]::details-content {
block-size: auto;
}
.panel { padding: 0 1rem 1rem; }
@media (prefers-reduced-motion: reduce) {
details::details-content { transition: none; }
}
</style>
</head>
<body>
<details>
<summary>What is included in the plan?</summary>
<div class="panel"><p>Unlimited projects, five collaborators and daily backups.</p></div>
</details>
<details>
<summary>Can I change plans later?</summary>
<div class="panel"><p>Yes. Upgrades apply immediately; downgrades apply at the next billing date.</p></div>
</details>
</body>
</html>
Two details make this work. First, the browser hides a closed <details> element's content with content-visibility: hidden on ::details-content, so that property must be transitioned with allow-discrete or the content would disappear the moment closing begins, before the height has had time to shrink. Second, overflow: clip keeps content from spilling out while the box is shorter than its content, without creating a scroll container.
The key technique: resolving keywords for interpolation
interpolate-size: allow-keywords changes one rule of the animation engine. Normally, when a transition's start or end value is an intrinsic sizing keyword — auto, fit-content, min-content, max-content — the browser cannot interpolate and the value flips discretely. With the opt-in, the browser resolves the keyword to the length it would compute right now and interpolates between lengths.
It is opt-in, rather than the default, for backward compatibility: some existing sites transition between a length and auto and rely on the snap. Setting it on :root enables it everywhere at once, which is almost always what a new project wants.
calc-size() is the related function for cases that need arithmetic on an intrinsic size. block-size: calc-size(auto, size + 1rem) means "the auto height plus a rem", and it is interpolable in its own right, even without interpolate-size. For plain auto targets the opt-in is simpler; calc-size() is for when the target itself must be modified.
Variation: the grid 0fr → 1fr fallback
Where interpolate-size is not supported, a grid provides nearly the same effect. A single-row grid whose row transitions from 0fr to 1fr animates between collapsed and content-height, because fractional track sizes are interpolable and a 1fr row in an auto-height grid is as tall as its content.
.collapsible {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 300ms cubic-bezier(0.22, 1, 0.36, 1);
}
.collapsible[data-open="true"] {
grid-template-rows: 1fr;
}
/* The single child must be allowed to shrink below its content. */
.collapsible > .collapsible__inner {
min-height: 0;
overflow: hidden;
}
@media (prefers-reduced-motion: reduce) {
.collapsible { transition: none; }
}
The min-height: 0 on the inner element is essential; without it, the grid item's automatic minimum keeps the row at content height and nothing collapses. The trick requires one extra wrapper element and a state hook — an attribute toggled by script, or a :has(:checked) selector on a checkbox-driven disclosure — so it is heavier than the details version but works in every browser that supports grid track animation. Combining both, with the grid version as the baseline and ::details-content inside @supports (interpolate-size: allow-keywords), gives smooth disclosures everywhere. The disclosure pattern itself is developed in CSS-Only Accordions and Disclosure.
Beyond height: other intrinsic sizes
Height is the famous case, but the opt-in covers every intrinsic sizing keyword in every sizing property, and several other components benefit.
Width to fit-content. A button whose label changes — "Save" becoming "Saving…" becoming "Saved" — can animate its width to the new label's size instead of jumping. With width: fit-content on the button and interpolate-size enabled, a transition on width smooths every label change.
Expanding search fields. A search input that grows from an icon-sized box to max-content or to a percentage when focused can transition between a fixed length and a keyword.
Tabs and segmented controls. An indicator that matches the width of the selected tab can size itself with fit-content on a positioned element, and animate its width as the selection moves between tabs of different lengths.
In each case the same caution applies: sizing properties drive layout, so the animation re-lays out the element and everything that depends on it each frame. Keep the affected subtree small — a button, a field, a tab bar — and the cost is negligible. Avoid applying it to large containers whose resize reflows the whole page, such as a sidebar whose width change pushes the main content across.
Performance and accessibility notes
Animating block-size is a layout animation: each frame recomputes the size of the panel and the position of everything after it. For a single accordion panel on a normal page that is inexpensive, but it cannot run on the compositor, so avoid animating many panels at once or panels with very complex content. Keep durations short — 200 to 300 milliseconds — so the layout work is brief.
The native <details> element carries the accessibility semantics: it is announced as a disclosure, toggled with Enter or Space, and its content is hidden from assistive technology when closed. The animation does not change any of that. Find-in-page can also open closed <details> elements to reveal matches, and the transition plays in that case too.
Browser support
interpolate-size and calc-size() are supported in Chrome and Edge 129+ and are not yet supported in Firefox or Safari. ::details-content is supported in Chrome and Edge 131+, Firefox 143+ and Safari 18.4+, so in Firefox and Safari the pseudo-element can be styled but its height snaps rather than animating. transition-behavior: allow-discrete is supported in Chrome and Edge 117+, Firefox 129+ and Safari 17.4+. The grid-row fallback works in every current engine. Guard the keyword version with @supports (interpolate-size: allow-keywords) if you combine the two.
FAQ
Why can't CSS transition height to auto?auto is a keyword, not a length, so the browser historically had no number to interpolate toward. interpolate-size: allow-keywords tells the browser to resolve intrinsic keywords such as auto and fit-content to lengths for interpolation, which makes the transition possible.
Where should interpolate-size be set?
On the root, as :root { interpolate-size: allow-keywords; }. The property inherits, so setting it once enables keyword interpolation everywhere. It is opt-in because enabling it globally could change the behaviour of existing transitions that relied on snapping.
What is calc-size() for?calc-size() lets you do arithmetic on an intrinsic size, such as calc-size(auto, size + 2rem), and it also makes that size interpolable. Use it when you need a modified intrinsic size; use interpolate-size when a plain auto target is enough.
What is the fallback for browsers without interpolate-size?
Wrap the content in a grid whose single row transitions from 0fr to 1fr. Fractional track sizes are interpolable everywhere, and a 1fr row sizes to its content, which gives the same visual effect as animating to auto.
Related
- Dialog & Popover Animations — the parent guide to discrete and intrinsic-size motion.
- CSS-Only Accordions and Disclosure — the disclosure pattern this animates.
- Transitioning display With allow-discrete — the discrete half of the same problem.
- min-content, max-content and fit-content — the intrinsic keywords that become interpolable.
Related articles
More pages in the same section.