albert.walickihire me
← all solutions
css

How to transition height from 0 to auto with CSS?

Animate an element from height 0 to its natural height with the grid-template-rows trick, the new interpolate-size property, or the old max-height hack.

transition: height doesn't animate to height: auto, because browsers couldn't calculate the values between a length and the auto keyword, so the element just jumps open. The version that works in every current browser wraps the content in a grid and transitions grid-template-rows from 0fr to 1fr:

<button class="faq-toggle" aria-expanded="false" aria-controls="faq-1">
  Do you ship abroad?
</button>

<div class="faq-panel" id="faq-1">
  <div class="faq-inner">
    <p>Yes, we ship to most countries in Europe.</p>
  </div>
</div>
.faq-panel {
  display: grid;
  grid-template-rows: 0fr;
  visibility: hidden;
  transition:
    grid-template-rows 300ms ease,
    visibility 300ms;
}

.faq-panel.is-open {
  grid-template-rows: 1fr;
  visibility: visible;
}

.faq-inner {
  overflow: hidden;
  min-height: 0;
}
const toggle = document.querySelector('.faq-toggle');
const panel = document.querySelector('.faq-panel');

toggle.addEventListener('click', () => {
  const isOpen = panel.classList.toggle('is-open');
  toggle.setAttribute('aria-expanded', isOpen);
});

How the grid trick works

The panel is a grid with a single row. A 1fr row is as tall as the content inside it, and a 0fr row gets none of that height. Unlike auto, fr values are numbers, so the browser can animate between them, and the content's real height is calculated for you.

The inner element needs overflow: hidden, so the content doesn't spill out of the collapsing row. min-height: 0 makes it explicit that the grid item is allowed to shrink below its content height.

Two details to get right:

  • Padding goes one level deeper. Vertical padding on .faq-inner stays visible when the panel is closed, because an element can't be smaller than its own padding. Put the padding on an element inside it, or on the <p>.
  • Hide closed content from keyboards and screen readers. A collapsed panel is only visually closed, so links inside it can still get focus. That's what visibility does in the example. When opening, it switches to visible straight away. When closing, it stays visible until the height transition is finished, then switches to hidden.

For people who have reduced motion turned on, put the transition inside @media (prefers-reduced-motion: no-preference) so the panel simply opens.

Newer: interpolate-size

There is now a property that makes the obvious code work. Set interpolate-size: allow-keywords once on :root, and transitions to and from keywords like auto animate:

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

.faq-panel {
  height: 0;
  overflow: hidden;
  transition: height 300ms ease;
}

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

At the time of writing, only Chromium browsers (Chrome, Edge, Opera) support it. In other browsers the panel still opens and closes, just without the animation, so it's safe to use as progressive enhancement. Check Can I Use for the current status. For now, I use the grid version when the animation matters everywhere.

The same property can animate the native <details> element in Chromium, through the ::details-content pseudo-element that wraps everything except the <summary>. Roughly like this:

details::details-content {
  height: 0;
  overflow: hidden;
  transition:
    height 300ms ease,
    content-visibility 300ms allow-discrete;
}

details[open]::details-content {
  height: auto;
}

Treat it as an enhancement too, and test it in the browsers you support.

The old max-height hack

Before these options, the common workaround was to animate max-height to a value that's surely bigger than the content:

.faq-panel {
  max-height: 0;
  overflow: hidden;
  transition: max-height 300ms ease;
}

.faq-panel.is-open {
  max-height: 1000px;
}

It works everywhere, but the timing is off because the max-height is a guess. If the content is 200px tall, it's fully visible after a fraction of the 300ms and the rest of the transition does nothing. Closing is worse: the transition first shrinks from 1000px to 200px with no visible change, so there's a delay before the panel starts to move. And if the content ever grows taller than your guess, it gets cut off. Use the grid version instead.

more solutions
work with me

Got something that needs building?

Frontend builds, full-stack features in Django, design-system work. Available for work.

See my workGet in touch