← Notes
5 Sept 2024 CSS

calc-size(): the fix for the hack we've been shipping for a decade

Animating height to auto has been impossible since CSS animations were invented. Chrome 129 ships calc-size(), which makes it possible. Here's the two-line fix and the surrounding context.

  • #css
  • #animation
  • #accordion
  • #layout

It’s 2016. You’re building an accordion. Product wants “smooth expand and collapse.” You write transition: height 300ms ease, set height: auto on the expanded state and the universe informs you that CSS cannot interpolate between a number and a keyword.

So you do what we all did. The measurement dance:

// The accordion hack, preserved in amber
function expand(panel) {
  const fullHeight = panel.scrollHeight; // read the natural height
  panel.style.height = '0';
  requestAnimationFrame(() => {
    panel.style.height = `${fullHeight}px`;
    panel.addEventListener('transitionend', () => {
      panel.style.height = 'auto'; // set to auto after animation so it's responsive
    }, { once: true });
  });
}

function collapse(panel) {
  panel.style.height = `${panel.scrollHeight}px`;
  requestAnimationFrame(() => {
    panel.style.height = '0';
  });
}

I have written this, roughly, forty times. Copy-pasted into projects. Found it in design system codebases. Watched junior developers discover it and get the specific expression of someone learning that what looked native was actually a workaround the whole time.

Chrome 129, August 2024. calc-size():

.accordion-panel {
  height: 0;
  overflow: hidden;
  transition: height 300ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

.accordion-panel.is-open {
  height: calc-size(auto, size);
}

calc-size(auto, size) resolves to the element’s natural auto height as an interpolatable value. The browser handles the measurement. No JavaScript. No scrollHeight. No rAF ceremony.

Browser support: Chrome 129+ (August 2024). Firefox and Safari: check MDN — neither has shipped this as of late 2024. Use @supports (height: calc-size(auto, size)) for progressive enhancement.

What size means

Inside calc-size(), size represents the resolved value of the keyword passed as the first argument:

/* Equivalent to the element's min-content height */
height: calc-size(min-content, size);

/* The element's fit-content height */
height: calc-size(fit-content, size);

/* 80% of the element's auto height */
height: calc-size(auto, size * 0.8);

/* The element's auto height plus 2rem of extra space */
height: calc-size(auto, size + 2rem);

You can do arithmetic with it. That’s the calc in calc-size.

A complete CSS accordion

<div class="accordion">
  <button class="accordion-trigger" aria-expanded="false" aria-controls="panel-1">
    Section one
  </button>
  <div class="accordion-panel" id="panel-1" role="region">
    <div class="accordion-panel-inner">
      Content here.
    </div>
  </div>
</div>
.accordion-panel {
  height: 0;
  overflow: hidden;
  transition: height 280ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

.accordion-panel.is-open {
  height: calc-size(auto, size);
}

@media (prefers-reduced-motion: reduce) {
  .accordion-panel { transition: none; }
}
// The only JavaScript you still write
document.querySelectorAll('.accordion-trigger').forEach(trigger => {
  trigger.addEventListener('click', () => {
    const panel = document.getElementById(trigger.getAttribute('aria-controls'));
    const isOpen = trigger.getAttribute('aria-expanded') === 'true';

    trigger.setAttribute('aria-expanded', String(!isOpen));
    panel.classList.toggle('is-open', !isOpen);
  });
});

Twelve lines. Every line is business logic: ARIA state, class toggle. No measurement, no geometry, no frame timing. The CSS does the interpolation.

The companion: interpolate-size

Chrome 129 also shipped interpolate-size: allow-keywords:

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

.accordion-panel {
  height: 0;
  transition: height 300ms ease;
}

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

With interpolate-size: allow-keywords on an ancestor, auto, min-content and fit-content become interpolatable directly. No calc-size() wrapper.

calc-size() is explicit: you opt in per property. interpolate-size is implicit: it enables keyword interpolation for everything in scope. Both work. calc-size() is surgical; interpolate-size is convenient.

See also: the dedicated note on interpolate-size for a deeper treatment of the keyword interpolation approach.

The decade we spent on this

The accordion problem is legitimate. It affects nearly every developer who has ever built a UI component. The scrollHeight hack is correct and reliable. It’s not broken. It just shouldn’t have existed.

The platform had the natural height, the transition mechanism and the intent. calc-size() is the bridge that finally connects them — and it’s worth understanding why it works, because the size keyword is more flexible than the accordion demo suggests.

Play with the arithmetic forms. calc-size(auto, size * 0.8) for partial reveals. calc-size(auto, size + 2rem) for breathing room. The keyword is a variable, not a fixed value.

Now delete the measurement code. You’ve earned it.