Interpolate-Size Lets CSS Animate Height to Auto Without Measuring Anything

interpolate-size and calc-size() finally let CSS transition elements to height: auto with zero JavaScript. Here is how the one-declaration API works, where it earns its keep, and the fallback for older browsers.

Interpolate-Size Lets CSS Animate Height to Auto Without Measuring Anything

Interpolate-Size Lets CSS Animate Height to Auto Without Measuring Anything

For the last decade, animating an element to height: auto meant one of two things: JavaScript that measures scrollHeight, or a max-height: 10000px hack that made your animation duration lie about when it finished. The CSSWG finally killed both workarounds with two small features that shipped across all major browsers over the past year: interpolate-size and calc-size().

Here is the short version. Add interpolate-size: allow-keywords to an element, and its size properties can now transition between a number and an intrinsic keyword like auto, min-content, max-content, or fit-content. That is the entire API. One declaration, and the browser handles the interpolation.

The property and its one value

interpolate-size takes two values: numeric-only (the default, which is why nothing has changed for you so far) and allow-keywords. It is an inherited property, which is how most teams will actually deploy it: declare it once on :root and every accordion, dropdown, and modal in the app gains the ability to animate to auto.

:root {
  interpolate-size: allow-keywords;
}

.accordion-panel {
  height: 0;
  overflow: hidden;
  transition: height 250ms ease-out;
}

.accordion-panel.open {
  height: auto;
}

That is the whole accordion. No ResizeObserver, no scrollHeight measurement in a requestAnimationFrame callback, no 0fr-to-1fr grid hack. The browser resolves the intrinsic height at the start of the transition and animates to it. The transitionend event fires when the animation genuinely ends, which is the part the max-height hack never managed.

calc-size(): the companion you probably need

interpolate-size solves the direct keyword-to-length transition. calc-size() solves the rest: it lets intrinsic keywords appear inside calc() and other math functions, where they were previously invalid. Before calc-size(), width: calc(100% - auto) was a syntax error. Now it parses, and it animates.

.tooltip {
  width: calc-size(max-content);
  max-width: min(80vw, calc-size(max-content));
  transition: width 200ms ease-out;
}

.tooltip.open {
  width: 0;
}

The two features are designed as a pair. interpolate-size controls whether keyword interpolation is allowed at all; calc-size() extends the set of valid expressions. If you ship one without the other, you will hit a wall: keyword values inside calc() only become animatable when both are in place.

Where it actually earns its keep

Accordions and collapsibles are the obvious win, but the feature is quiet and useful in smaller places. A details-like disclosure component can animate open and closed with nothing but a class toggle. A dropdown menu whose height depends on how many items it contains animates to the right height instead of a hard-coded one. A modal that grows with its content stops jumping: you set height: auto with a transition, and opening it animates from a collapsed state to whatever the content needs.

Auto width is where the old hacks really hurt. Tooltips and popovers with width: max-content could not be transitioned at all, so teams either skipped the animation or measured in JS on every open. With interpolate-size, width transitions to max-content just work, and content-driven widths stop being the thing you special-case.

There is also a design-system angle. Component libraries have shipped their own collapse utilities for years, each with its own measurement code and its own quirks. If your tokens layer can carry interpolate-size: allow-keywords as an opt-in, every consumer gets honest height animations for free, and the library authors can delete the measurement module entirely.

Gotchas before you ship it

First, it is inherited, and that cuts both ways. Setting it on :root is convenient, but it changes the behavior of every size transition in the document, including third-party components you do not control. If a library transitions height with an auto target it previously could not have interpolated, its timing changes. Scope the declaration to the components you wrote if you are nervous about that blast radius.

Second, animating height or width is still a layout animation. The browser re-lays-out the element and everything after it in the document on every frame. That is fine for a 400px accordion panel. It is not fine for a full-page container whose children are a virtualized list of ten thousand rows. For those, stick with the 0fr grid trick or a transform-based reveal, where the per-frame cost is lower.

Third, the intrinsic target is resolved at the start of the transition. If the content inside the panel changes while it is animating, the panel will not follow the new height until the next transition fires. For content that changes mid-animation, you still need to re-trigger the transition or measure manually. This is the one case where the old JavaScript is not entirely dead.

And the overflow: hidden you need for the collapse effect clips box-shadows and breaks position: sticky inside the panel. Plan for it the way you always have, because it is the same cost as every height animation before it.

Fallback and browser support

Chrome shipped interpolate-size in version 129, Firefox in 138, and Safari in 26, with calc-size() alongside in each engine, so as of mid-2026 all three major browsers support both. That is Baseline "widely available" territory. For the long tail, the grid-template-rows: 0fr to 1fr animation is a solid fallback that has worked everywhere since 2021, and you can gate the two approaches with @supports:

@supports (interpolate-size: allow-keywords) {
  .panel { transition: height 250ms; }
  .panel.open { height: auto; }
}

@supports not (interpolate-size: allow-keywords) {
  .panel {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows 250ms;
  }
  .panel.open { grid-template-rows: 1fr; }
}

The last time CSS gave us a reason to delete JavaScript, we deleted a lot of it. This is a smaller moment, but it is the same trade: a hundred lines of scrollHeight measurement in your component library, replaced by one inherited declaration and a transition. The measurement code is gone, and the animation is finally honest about when it ends.

Comments