Let’s properly understand this. Lemme ask you something: how many pixels tall is this list below?
<ul>
<li>How</li>
<li>Tall</li>
<li>Am I?</li>
</ul>
Code language: HTML, XML (xml)
Trick question. You can’t possibly know (from CSS, anyway). It depends on the font family, font size, layout choices, user preferences and overrides, screen size, and much more. Because CSS doesn’t know, it can’t animate to it. It feels silly because c’mon it’s right there on the page, but that’s just the way it is.
For many, many years, one of the most common wishes for CSS developers is some way to animate an element from hidden (or newly added to the page) as zero-height to whatever it’s natural intrinsic size is. “Animate to auto” or “transition to auto” is how it’s often talked about. The desire is pretty straightforward:
.element {
height: 0; /* or block-size */
transition: height 0.2s ease-in-out;
&.open {
/* nope, sorry, transition will not happen */
height: auto;
}
}
Code language: CSS (css)
This kind of thing is essentially an open or close animation, which is the most common animation of all. Check the boxes below to see how only the box with a known height can have its height animated:
There are work arounds, like Just Using JavaScript™️ (e.g. measure the size off screen then animate to that). Or, animating the max-height
instead, which sort of works but it messes with the timing as it’s likely part of the animation will be animating to a number too-high or too-low.
To be fair, we sort of got this when we got View Transitions. (See this example). But same-page View Transitions require JavaScript and are not as ergonomic as just using the basic CSS.
CSS is poised to beat this boss battle soon, which is an incredible thing to see. It needs to be able to do two things:
- Animate from zero-height (or being just-added to the DOM) to the intrinstic (
auto
) size. Mostlyheight
/block-size
, but the other direction is helpful too. - Animate from an intrinsic (
auto
) size back to zero.
As I write, there is a (very experimental) version of this working in Chrome Canary. It’s all solved with one little line:
.element {
height: 0; /* or block-size */
transition: height 0.2s ease-in-out;
&.open {
/* works now! 🎉 */
height: calc-size(auto, size);
}
}
Code language: JavaScript (javascript)
If you’ve got a copy of Chrome Canary with Experimental Web Platform Features flag on you’ll see this work right now! (UPDATE: Seems to be working for me in stable Chrome (129) without any special flags.)
A Real World Example: Dropdowns
Dropdown menus are a good example as they can have in them a different number of elements and thus have an unknown height, and yet you may want to animate them open. In the demo below, dropdown menus are exactly what I’ve made. The submenus are hidden by virtue of being absolutely positioned and of zero height. I find you generally don’t want to display: none
a submenu as then it makes tabbing through the menu difficult or impossible.
The CSS is heavily annotated above to explain each interesting bit. Here’s a video of it all working Chrome Canary:
Note that this menu intentionally doesn’t use display: none
to hide the submenus for accessibility reasons. If you do need to also transition an element from the display: none
state (or just being added to the DOM for the first time), that adds more complication. But amazingly, modern CSS is up for that job also, as we’ll see next.
display: none;
Transitioning from Let me just show you the code:
.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-size(auto, size);
}
}
Code language: CSS (css)
The transition-behavior: allow-discrete;
(mind-bending name that I do not understand) allows the display
property to swap where it updates during the transition. Instead of the normal behavior of changing immediately (thus preventing any animation) it changes at the end (and vice-versa when animating from hidden).
Then we also need @starting-style
here, which duplicates the “closed” styling. That seems awfully weird too, but this is how it’s going to work (there is real browser support for this). A way that helps me think about it is that when display: none
is in use, none of the other styles are really applied to the element, it’s just not there. When the open
class is applied here, all those styles are immediately applied. It had no prior state. The open state is the only state. So @starting-style
is a way to force in a prior state.
Adam Argyle was experimenting with some globally applied styles using @starting-style
with some generic styles such that any element appearing on the page gets a bit of a scale up and fade in.
The idea started like this:
* {
transition: opacity .5s ease-in;
@starting-style { opacity: 0 }
}
Code language: CSS (css)
And then with a bit more nuance and care ended up like this:
@layer {
* {
@media (prefers-reduced-motion: no-preference) {
transition:
opacity .5s ease-in,
scale .5s ease-in,
display .5s ease-in;
transition-behavior: allow-discrete;
}
@starting-style {
opacity: 0;
scale: 1.1;
}
&[hidden],
dialog:not(:modal),
&[popover]:not(:popover-open) {
opacity: 0;
scale: .9;
display: none !important;
transition-duration: .4s;
transition-timing-function: ease-out;
}
}
}
Code language: CSS (css)
You might also enjoy watching Zoron Jambor’s video about all this if you like taking in information that way!
Update September 2024
Bramus’ article covers how this ended up. I’ve updated this article to be in line with all that. The calc-size()
function still exists, you just now pass an additional size
parameter to it for it to work. But you don’t always need it, as interpolate-size: allow-keywords;
will allow the animation to sizing keywords to work without it.
html {
/* This allows auto to be interpolated to,
along with min-content, max-content, and fit-content.
It cascades as needed. */
interpolate-size: allow-keywords;
}
.el {
inline-size: 80px;
overflow-x: clip;
transition: inline-size 0.35s ease;
&:hover,
&:focus-visible {
/* Requires interpolate-size: allow-keywords; */
inline-size: max-content;
/* Works without, and allows for calc() stuff */
inline-size: calc-size(max-content, size);
}
}
Code language: JavaScript (javascript)
It is also possible to animate from height 0 to auto if you use grid and animate from 0fr tro 1fr. I already used this trick in several projects.
Oh yeah! I was so stoked when I learned that. https://css-tricks.com/css-grid-can-do-auto-height-transitions/
I would argue that submenus should be hidden with display:none for accessibility reasons. A screen reader or keyboard user shouldn’t need to tab through all of the submenu options by default. Display:none would hide the menus until the user is ready to go deeper, perhaps by clicking a parent toggle. Great post btw!
I dunno, I’ve always heard/thought the opposite. But I can see the logic here too. Probably worth consulting some actual screen reader users.
I think this is what
inert
(https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inert) attempts to solve.Thanks so much for recommending my video guide, Chris! It means a lot! 🙏