I know this is something Chris has wanted forever, so it’s no surprise he’s already got a fantastic write-up just a day after the news broke. In fact, I first learned about it from his post and was unable to dredge up any sort of announcement. So, I thought I’d jot some notes down because it feels like a significant development.
The news: transitioning to auto
is now a thing! Well, it’s going to be a thing. Chrome Canary recently shipped support for it and that’s the only place you’ll find it for now. And even then, we just don’t know if the Chrome Canary implementation will find its way to the syntax when the feature becomes official.
The problem
Here’s the situation. You have an element. You’ve marked it up, plopped in contents, and applied a bunch of styles to it. Do you know how tall it is? Of course not! Sure, we can ask JavaScript to evaluate the element for us, but as far as CSS is concerned, the element’s computed dimensions are unknown.
That makes it difficult to, say, animate that element from height: 0
to height: whatever
. We need to know what “whatever” is and we can only do that by setting a fixed height on the element. That way, we have numbers to transition from zero height to that specific height.
.panel {
height: 0;
transition: height 0.25s ease-in;
&.expanded {
height: 300px;
}
}
But what happens if that element changes over time? Maybe the font changes, we add padding, more content is inserted… anything that changes the dimensions. We likely need to update that height: 300px
to whatever new fixed height works best. This is why we often see JavaScript used to toggle things that expand and contract in size, among other workarounds.
I say this is about the height
property, but we’re also talking about the logical equivalent, block-size
, as well as width
and inline-size
. Or any direction for that matter!
auto
Transitioning to That’s the goal, right? We tend to reach for height: auto
when the height dimension is unknown. From there, we let JavaScript calculate what that evaluates to and take things from there.
The current Chrome implementation uses CSS calc-size()
to do the heavy lifting. It recognizes the auto
keyword and, true to its name, calculates that number. In other words, we can do this instead of the fixed-height approach:
.panel {
height: 0;
transition: height 0.25s ease-in;
&.expanded {
height: calc-size(auto);
}
}
That’s really it! Of course, calc-size()
is capable of more complex expressions but the fact that we can supply it with just a vague keyword about an element’s height is darn impressive. It’s what allows us to go from a fixed value to the element’s intrinsic size and back.
I had to give it a try. I’m sure there are a ton of use cases here, but I went with a floating button in a calendar component that indicates a certain number of pending calendar invites. Click the button, and a panel expands above the calendar and reveals the invites. Click it again and the panel goes back to where it came from. JavaScript is handling the click interaction, triggering a class change that transitions the height in CSS.
A video in case you don’t feel like opening Canary:
This is the relevant CSS:
.invite-panel {
height: 0;
overflow-y: clip;
transition: height 0.25s ease-in;
}
On click, JavaScript sets auto height on the element as an inline style to override the CSS:
<div class="invite-panel" style="height: calc(auto)">
The transition
property in CSS lets the browser know that we plan on changing the height
property at some point, and to make it smooth. And, as with any transition or animation, it’s a good idea to account for motion sensitivities by slowing down or removing the motion with prefers-reduced-motion
.
display: none
?
What about This is one of the first questions that popped into my head when I read Chris’s post and he gets into that as well. Transitioning from an element from display: none
to its intrinsic size is sort of like going from height: 0
. It might seem like a non-displayed element has zero height, but it actually does have a computed height of auto
unless a specific height is declared on it.
So, there’s extra work to do if we want to transition from display: none
in CSS. I’ll simply plop in the code Chris shared because it nicely demonstrates the key parts:
.element {
/* hard mode!! */
display: none;
transition: height 0.2s ease-in-out;
transition-behavior: allow-discrete;
height: 0;
@starting-style {
height: 0;
}
&.open {
height: calc(auto);
}
}
- The element starts with both
display: none
andheight: 0
. - There’s an
.open
class that sets the element’s height tocalc-size(auto)
.
Those are the two dots we need to connect and we do it by first setting transition-behavior: allow-discrete
on the element. This is new to me, but the spec says that transition-behavior
“specifies whether transitions will be started or not for discrete properties.” And when we declare allow-discrete
, “transitions will be started for discrete properties as well as interpolable properties.”
Well, DevTools showed us right there that height: auto
is a discrete property! Notice the @starting-style
declaration, though. If you’re unfamiliar with it, you’re not alone. The idea is that it lets us set a style for a transition to “start” with. And since our element’s discrete height is auto
, we need to tell the transition to start at height: 0
instead:
.element {
/* etc. */
@starting-style {
height: 0;
}
}
Now, we can move from zero to auto
since we’re sorta overriding the discrete height with @starting-style
. Pretty cool we can do that!
This is amazing. Countless times I have wished auto could be a viable animation target.
I was using @starting-style a few months back to animate in the display of a native
<dialog>
(nothing crazy, just opacity and translate), and it kept causing chrome tabs to crash with the “Aw snap” message.So be careful out there on the edge, folks.
Not many people know it, but this use case was already supported with CSS grid. A grid with an unspecified height has grid tracks with a height of 1fr. So, you can transition from 0fr to 1fr smoothly.
Didn’t we already figure this out with css-grid?:
First I want to point out a typo that threw me off (until I understood that there’s a typo)
> […] but it actually does have a computed height or auto unless a specific height is declared on it.
“computed height or auto” should be “computed height of auto”
And second: I think you got the explanation wrong why
transition-behavior: allow-discrete;
is needed whendisplay: none;
is involved. It seems in Chris’ article the part that switches fromdisplay: none
todisplay: something
was omitted or at least I didn’t see it, but as far as I understood it, the style or code that sets it would switch the property immediately, so we would never be able to see a closing animation ofheight
. Withtransition-behavior: allow-discrete;
the actual switch todisplay: none
takes effect only after any animation / transition has finished. (And it behaves the other way around when a style changes an element fromdisplay: none
todisplay: something
.)The
@starting-style
is needed because as you have mentioned an element withdisplay: none
ends up being renderd with a computed style ofheight: auto
regardless of the property set toheight: 0
via the style. Unintuitive yes, but probably ancient, spec’d, established browser behavior.Which would mean that we would attempt to transition from
auto
toauto
which will do nothing. Hence@starting-style { height: 0; }
.And third: thank you for the article.
Yay. CSS animations for opening/closing
<details><summary>
blocks without a line of JS!I also wrote on Stackoverflow about this regarding the transitioning of the
display
property: https://stackoverflow.com/a/78304935/104380Instead of using height, I used
max-height: 0;
and transitioned it tomax-height: 1000px;
(larger than the content could become) for many, many years. This was working perfectly without the fixed height. But it was a hack. I’m glad I don’t need it anymore.