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
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
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 nestedcalc-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: