Short answer: Wrap the content in a grid and animate the row instead of the height: grid-template-rows: 0fr when closed, 1fr when open, with transition: grid-template-rows 0.35s, and min-height: 0; overflow: hidden on the one child. It works in every current browser. The newer interpolate-size: allow-keywords lets height: auto animate directly, but not every browser has it yet.
You write transition: height 0.3s, toggle from height: 0 to height: auto, and the panel jumps open without any animation. It is not a bug in your code: browsers cannot work out the values between 0 and "whatever the content needs", so for years auto simply could not be animated.
Why does height: auto not animate?
A transition needs two numbers to move between. auto is not a number until the browser has laid out the content, so the classic rule was: no transition to or from auto. Widths and heights in fr units of a grid are different: 0fr and 1fr are numbers, the grid works out the real height for each step, and the panel opens smoothly to exactly its content's height.
The grid trick: an accordion that works everywhere
This is Motion Studio's accordion. Only the button opens and closes it, the button tells screen readers whether it is open (aria-expanded), and the closed panel's links are kept out of the Tab order (visibility):
<div class="accordion">
<button type="button" aria-expanded="false">What is included?<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 9.5l6 6 6-6" />
</svg></button>
<div>
<div>
<p>Every motion, its code in four formats, and a GIF or a video of it.</p>
</div>
</div>
</div>.accordion {
width: 240px;
overflow: hidden;
border: 1px solid #1d1a261a;
border-radius: 12px;
background: #ffffff;
color: #1d1a26;
font-family: inherit;
}
.accordion button {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
width: 100%;
padding: 12px 14px;
border: 0;
background: none;
color: inherit;
font: inherit;
font-size: 14px;
font-weight: 600;
text-align: left;
cursor: pointer;
}
.accordion svg {
flex: none;
width: 18px;
height: 18px;
fill: none;
stroke: #d864ff;
stroke-width: 2.5;
stroke-linecap: round;
stroke-linejoin: round;
transition: transform 0.35s cubic-bezier(.65, 0, .35, 1);
}
.accordion>div {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.35s cubic-bezier(.65, 0, .35, 1);
}
.accordion>div>div {
min-height: 0;
overflow: hidden;
visibility: hidden;
transition: visibility 0.35s;
}
.accordion p {
margin: 0;
padding: 0 14px 12px;
font-size: 13px;
line-height: 1.5;
opacity: .7;
}
.accordion.is-on svg {
transform: rotate(180deg);
}
.accordion.is-on>div {
grid-template-rows: 1fr;
}
.accordion.is-on>div>div {
visibility: visible;
}
@media (prefers-reduced-motion:reduce) {
.accordion, .accordion *, .accordion::before, .accordion::after, .accordion *::before, .accordion *::after {
animation: none !important;
transition: none !important;
}
}document.querySelectorAll('.accordion').forEach(el => {
const trigger = el.querySelector('button');
trigger.addEventListener('click', () => {
const on = el.classList.toggle('is-on');
trigger.setAttribute('aria-expanded', on);
});
});The three lines that do the work:
| Rule | Why it is needed |
|---|---|
grid-template-rows: 0fr to 1fr | The animatable stand-in for height 0 to auto |
min-height: 0 on the child | A grid item will not shrink below its content without it, so 0fr would stay open |
overflow: hidden on the child | Hides the content while the row is shorter than it |
The visibility transition is the detail most copies leave out. Without it, a closed panel is invisible but its links can still be reached with Tab, and a keyboard user lands on something they cannot see. visibility switches at the end of the closing transition and at the start of the opening one, so the animation still shows.
The new way: details and interpolate-size
The <details> element opens and closes by itself, without JavaScript, and two newer CSS features let it animate:
<details>
<summary>What is included?</summary>
<p>Every motion, its code in four formats, and a GIF or a video of it.</p>
</details>details {
interpolate-size: allow-keywords;
}
details::details-content {
block-size: 0;
overflow: hidden;
transition: block-size 0.35s, content-visibility 0.35s allow-discrete;
}
details[open]::details-content {
block-size: auto;
}::details-content is the part of <details> that opens and closes; MDN marks it Baseline 2025, newly available in current browsers. interpolate-size: allow-keywords is what allows the animation to auto, and MDN still lists it as limited availability. In Chrome 153 the panel animates; Firefox 155, which does not support interpolate-size yet, opens and closes it at once. That is a fine fallback: nothing breaks, it just does not move.
What about the max-height hack?
The old workaround animates max-height from 0 to a number bigger than the content, such as 500px. It works, badly: the transition runs over the whole 500px, so a 100px panel opens in a fraction of the time you set and closes after a pause while the invisible extra height runs out. If the content ever grows past your guess, it gets cut off. The grid trick has none of these problems.
Which one should you use?
| Way | Browsers | JavaScript | Use it when |
|---|---|---|---|
Grid rows 0fr to 1fr | All current ones | A few lines to toggle | You want the animation everywhere today |
<details> with ::details-content | All current ones open it; the animation needs interpolate-size | None | An FAQ where the animation is a bonus |
max-height | All | A few lines | Never, if you can avoid it |
Where can you check this?
Can you change the colours and timing without editing CSS?
Motion Studio has the Accordion with its chevron colour, question, duration and easing as settings, among 150 motion graphics. It exports the result as HTML and CSS, React, Vue or Svelte, a GIF or a video, with the reduced-motion rule already in the code.

