Animate to height: auto; (and other intrinsic sizing keywords) in CSS

Use the interpolate-size property or the calc-size() function to enable smooth transitions and animations from lengths to intrinsic sizing keywords and back.

Published: Sep 17, 2024

Introduction

An often requested CSS feature is the ability to animate to height: auto. A slight variation of that request is to transition the width property instead of the height, or to transition to any of the other intrinsic sizes represented by keywords like min-content, max-content, and fit-content.

For example, in the following demo it would be nice if the labels would smoothly animate to their natural width when hovering the icons.

The CSS used is the following:

nav a {
    width: 80px;
    overflow-x: clip;
    transition: width 0.35s ease; /* 👈 Transition the width */

    &:hover,
    &:focus-visible {
        width: max-content; /* 👈 Doesn't work with transitions */
    }
}

Even though a transition is declared to transition the width property, and width: auto is declared on :hover, no smooth transition happens. Instead, the change is abrupt.

Animate to and from intrinsic sizing keywords with interpolate-size

Browser Support

  • Chrome: 129.
  • Edge: not supported.
  • Firefox: not supported.
  • Safari: not supported.

The CSS interpolate-size property gives you control over whether animations and transitions of CSS intrinsic sizing keywords should be allowed or not.

Its default value is numeric-only which does not enable interpolation. When setting the property to allow-keywords, you opt-in to interpolations from lengths to CSS intrinsic sizing keywords in the cases where the browser can animate those keywords.

As per spec:

  • numeric-only: An <intrinsic-size-keyword> cannot be interpolated.
  • allow-keywords: Two values can be interpolated if one of them is an <intrinsic-size-keyword> and the other is a <length-percentage>. […]

Because the interpolate-size property is one that inherits, you can declare it on :root to enable transitioning to and from intrinsic sizing keywords for the entire document. This is the recommended approach.

/* Opt-in the whole page to interpolate sizes to/from keywords */
:root {
    interpolate-size: allow-keywords; /* 👈 */
}

In the following demo, this rule is added to the code. As a result, the animations to and from width: auto work fine (in browsers with support):

Limit the reach of the opt-in by narrowing down the selector

If you want to limit the allow-keywords opt-in to only a subtree of your document, adjust the selector from :root to only the element that you want to target. For example, in case the <header> of your page is not compatible with these type of transitions, you could limit the opt-in to only the <main> element and its descendants as follows:

main { /* 👈 Scope the opt-in to only <main> and its descendants */
    interpolate-size: allow-keywords;
}

Why not allow animation to and from sizing keywords by default?

A common piece of feedback on this opt-in mechanism is that browsers should just allow transitions and animations from intrinsic sizing keywords to lengths by default.

The option to enable this behavior was researched during the development of the feature. The working group discovered that enabling this by default is not backward compatible because many style sheets assume that intrinsic sizing keywords (such as auto or min-content) cannot be animated. You can find the details in this comment on the relevant CSS Working Group issue.

Therefore the property is an opt-in. Thanks to its inheritance trait, opting in an entire document is merely a interpolate-size: allow-sizes declaration on :root as detailed previously.

Animate to and from intrinsic sizing keywords with calc-size()

Browser Support

  • Chrome: 129.
  • Edge: 129.
  • Firefox: not supported.
  • Safari: not supported.

Another way to enable interpolation to and from intrinsic sizing keywords is to use the calc-size() function. It allows mathematics to be performed on intrinsic sizes in a safe, well-defined way.

The function accepts two arguments, in order:

  • A calc-size basis, which can be an <intrinsic-size-keyword> but also a nested calc-size().
  • A calc-size calculation, which lets you perform calculations using the calc-size basis. To refer to the calc-size basis, use the size keyword.

Here are some examples:

width: calc-size(auto, size);        // = the auto width, unaltered
width: calc-size(min-content, size); // = the min-content width, unaltered

Adding calc-size() to the original demo, the code looks like this:

nav a {
    width: 80px;
    overflow-x: clip;
    transition: width 0.35s ease;

    &:hover,
    &:focus-visible {
        width: calc-size(max-content, size); /* 👈 */
    }
}

Visually, the outcome is exactly the same as when using interpolate-size. So in this specific case you should use interpolate-size.

Where calc-size() does shine is its ability to do calculations, which is something that can't be done with interpolate-size:

width: calc-size(auto, size - 10px); // = The auto width minus 10 pixels
width: calc-size(min-content, size + 1rem); // = The min-content width plus 1rem
width: calc-size(max-content, size * .5);   // = Half the max-content width

For example, if you want all paragraphs on a page to be sized to the nearest multiple of 50px, you can use the following:

p {
    width: calc-size(fit-content, round(up, size, 50px));
    height: calc-size(auto, round(up, size, 50px));
}

What calc-size() also lets you do is to interpolate between two calc-size()s when both their calc-size bases are identical. This too is something that can't be achieved with interpolate-size.

#element {
    width: min-content; /* 👈 */
    transition: width 0.35s ease;

    &:hover {
        width: calc-size(min-content, size + 10px); /* 👈 */
    }
}

Why not allow <intrinsic-size-keyword> in calc()?

A question that commonly pops up with calc-size() is why the CSS Working Group didn't adjust the calc() function to support intrinsic sizing keywords.

One of the reasons for this is that you are not allowed to mix and match intrinsic sizing keywords when doing calculations. For example, you could be tempted to write calc(max-content - min-content) which looks valid, but in reality it is not. calc-size() enforces correctness because it, unlike calc(), accepts only one single <intrinsic-size-keyword> as its first argument.

Another reason is context-awareness. Some layout algorithms have a special behavior for specific intrinsic sizing keywords. calc-size() is explicitly defined to represent an intrinsic size, not a <length>. Thanks to this, those algorithms are able to treat calc-size(<intrinsic-size-keyword>, …) as the <intrinsic-size-keyword>, maintaining its special behavior for that keyword.

Which approach to use?

In most cases, declare interpolate-size: allow-keywords on :root. It's the easiest way to enable animation to and from intrinsic sizing keywords as it's essentially a one-liner.

/* Opt-in the whole page to animating to/from intrinsic sizing keywords */
:root {
    interpolate-size: allow-keywords; /* 👈 */
}

This piece of code is a nice progressive enhancement, as browsers that don't support it will fall back to using no transitions.

When you need finer grained control over things–such as doing calculations–or you want to use a behavior only calc-size() can do, you can resort to using calc-size().

#specific-element {
    width: 50px;

    &:hover {
        width: calc-size(fit-content, size + 1em); /* 👈 Only calc-size() can do this */
    }
}

However, using calc-size() in your code will require you to include fallbacks for browsers that don't support calc-size(). For example, adding extra size declarations, or falling back to feature detection using @supports.

width: fit-content;
width: calc-size(fit-content, size + 1em);
       /* 👆 Browsers with no calc-size() support will ignore this second declaration,
             and therefore fall back to the one on the line before it. */

More demos

Here are some more demos that use interpolate-size: allow-keywords to their advantage.

Notifications

The following demo is a fork of this @starting-style demo. The code was adjusted to allow items with varying heights to be added.

To achieve this, the whole page opts in to size keyword interpolation and the height on each .item element is set to auto. Otherwise, the code is exactly the same as from before forking.

:root {
    interpolate-size: allow-keywords; /* 👈 */
}

.item {
    height: auto; /* 👈 */

    @starting-style {
        height: 0px;
    }
}

Animate the <details> element

A typical use-case where you'd want to use this type of interpolation is to animate a disclosure widget or an exclusive accordion as it opens. In HTML, you use the <details> element for this.

With interpolate-size: allow-keywords you can get pretty far:

@supports (interpolate-size: allow-keywords) {
    :root {
        interpolate-size: allow-keywords;
    }
    
    details {
        transition: height 0.5s ease;
        height: 2.5rem;
        
        &[open] {
            height: auto;
            overflow: clip; /* Clip off contents while animating */
        }
    }
}

As you can see, though, the animation only runs when the disclosure widget is opening. To cater for this, Chrome is working on the ::details-content pseudo which will ship in Chrome later this year (and which will be covered in a future post). Combining interpolate-size: allow-keywords and ::details-content, you can get an animation in both directions: