Search

One of the Boss Battles of CSS is Almost Won! Transitioning to Auto

TL;DR: The experimental CSS function calc-size(auto) can be used so that transitions and animations can go from zero (0) to this value. But that is unlikely to be the final syntax! ⚠️ So be forewarned.

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. Mostly height / 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);
  }
}
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!

Fair Warning: This is Probably Going to Break

I warned you. This is experimental stuff. I’m told that calc-size() probably is not going to be how this all ends up. It may be that there is an “opt-in” property that has to be set (then cascades) for this to work. It’s all early and unclear, and that’s fine because that’s how the best solutions happen.

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.

Transitioning from display: none;

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);
  }
}
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!

Wanna learn CSS Animations deeply?

6 responses to “One of the Boss Battles of CSS is Almost Won! Transitioning to Auto”

  1. Avatar JayJayCapone says:

    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.

  2. Avatar Josh Dreier says:

    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!

  3. Avatar Zoran Jambor says:

    Thanks so much for recommending my video guide, Chris! It means a lot! 🙏

Leave a Reply

Your email address will not be published. Required fields are marked *

Frontend Masters ❤️ Open Source

Did you know? Frontend Masters Donates to open source projects. $313,806 contributed to date.