Written by Saleh Mubashar✏️
Until recently, only a limited number of CSS properties could be animated. For example, to create a fade-in or fade-out effect, you would typically use the opacity
property instead of the display
property, as the latter could not be animated. The issue, however, is that while the element becomes visually hidden, it’s still present on the page.
Recently, Chrome introduced new features that resolve this issue and make the development process much simpler. In this article, we'll compare the traditional methods for animating the display and size properties with these new features.
The problem with animating display
and element size
Chances are, you’ve had to create a fade-in/out effect on some element using CSS at some point. The go-to method is to apply an animation or transition to the element’s opacity
. But setting the opacity to zero doesn’t actually remove the element — it just makes it invisible. Most of the time, that’s good enough.
But let’s say you’ve got a to-do list where users can delete items. If you want to create an exit animation such that the item fades out, you would normally use opacity
. But if the list needs to adjust its height, you’d also need to set the display
to none
. The issue here is that while the item is visually gone, it still takes up space in the DOM and messes with things like layout and user interactions.
Here’s a side-by-side comparison of two approaches: one using just opacity
, and the other combining opacity
with display
. You can try the example below to see the differences:
See the Pen Simple Todo App Comparison by Saleh-Mubashar (@saleh-mubashar) on CodePen.
Notice how the layout shifts when combining display
with opacity
, while using opacity
alone leaves gaps in the list. While the second method (opacity
+ display
) solves the layout issue, it interferes with the smooth fade-out effect because display: none
is applied before the fade finishes. This causes a sudden disappearance rather than a gradual fade.
For example, the opacity
property can transition smoothly from 0
to 1
. However, the display
property cannot be animated because it doesn’t have a numeric range — its states are binary, like none
, block
, or other values. Because there’s no in-between value, CSS can't animate the display
.
Similarly, developers often face challenges when trying to animate the intrinsic size of an element, such as height: auto
. This is commonly used for transitions on collapsible sections like accordions, where the height starts at 0px
when closed and expands to fit the content when opened. Although size properties like height
can typically be animated (because they have numeric start and end values), animating to or from auto
creates issues. The browser can’t calculate the steps between 0px
and auto
; thereby complex workarounds must be used.
The traditional solutions for animating display
and size
There are several ways to address the challenges of animating display
and element sizes. In this section, we’ll discuss the most popular solutions using both CSS and JavaScript.
CSS-based solutions
There are a couple of ways to solve the issue of the display property not being animatable using CSS. The most reliable one is using opacity
along with a size property such as height
or width
. In this case, the size property is used to effectively remove the element from the DOM. This can be done using the transition-delay
property. Basically, we add a delay to the size transition, which is equal to the time set for the opacity transition. Once the element fades out, its size is immediately set to zero, effectively removing it from the layout as if display: none
had been applied.
Using the to-do list as an example again, the implementation would look somewhat like this:
li {
height: 50px; /* any measurable value, not "auto" */
opacity: 1;
transition: height 0ms 0ms, opacity 400ms 0ms;
}
.fade-out {
overflow: hidden; /* Hide the element content, while height = 0 */
height: 0;
opacity: 0;
padding: 0;
transition: height 0ms 400ms, padding 0ms 400ms, opacity 400ms 0ms;
}
Here, the trick is setting the height
and padding
to 0
after a delay once the opacity fades to 0
. The delay and length of the opacity need to be the same — in this case, 400ms. The height: 0
makes sure that the list item does not interact with the layout. As discussed earlier, height: auto
adjusts dynamically based on content; hence, it cannot be animated. Therefore, you need to ensure that the element has a specific, fixed height for the animation to work properly.
Setting the visibility to hidden is another commonly used method. However, this does not remove the element from the DOM, and it still affects the layout as normal, i.e., it influences the positioning of surrounding elements.
The most common CSS solution for animating an element to or from its intrinsic size (or height: auto
) is to use max-height
instead of height
. It’s not the cleanest implementation, but it gets the job done. You basically set the max-height
to a value larger than the element will ever get. This way, it mimics a smooth transition, similar to animating a fixed height:
.collapsible {
max-height: 0;
overflow: hidden;
transition: max-height 0.4s ease;
}
.collapsible.open {
max-height: 500px;
}
The most obvious downside to this approach is that you have to make sure the max-height
is always larger than the actual content inside the element. Another issue is that the transition timing can feel inaccurate unless the content height perfectly matches the max-height
value.
Let’s say that your content is 400px high but you set the max-height
to 1000px. The animation will technically continue for the entire duration (let’s say two seconds). But visually, the element will stop growing as soon as it reaches the actual height of the content (400px), while the max-height
keeps transitioning to 1000px. So, in this case, the transition duration will be shorter than the one you specify.
JavaScript methods
All the CSS solutions discussed above are quite complex and can lead to unpredictable results. Until recently, the most reliable way to achieve this was through JavaScript.
To apply display none
after an opacity transition, we can use either the setInterval
or setTimeout
function to add a delay that matches the opacity transition duration. After this delay, you can set the display to none
. Here’s an example:
document.getElementById("fadeButton").addEventListener("click", function () {
const element = document.getElementById("myElement");
element.style.opacity = "0";
setTimeout(() => {
element.style.display = "none";
}, 1000); // Match this value with the duration in CSS
});
In this code, after the button is clicked, the element fades out over one second, and then its display
is set to none
immediately — essentially removing it from the layout.
Similarly, to animate the intrinsic size, we can calculate the height of the element in JavaScript and use that value as an endpoint for the height. This approach is much more reliable and precise. However, keep in mind that we are still animating on the height
property.
The obvious benefit here is that you’re dynamically setting the height based on the element’s actual content, ensuring the transition matches the real height rather than guessing with max-height
.
Here’s how you can do it:
document.getElementById("toggleButton").addEventListener("click", function () {
const el = document.querySelector(".expandable");
const contentHeight = el.scrollHeight;
el.style.height = contentHeight + "px";
el.addEventListener("transitionend", function (e) {
el.removeEventListener("transitionend", arguments.callee);
el.style.height = "auto";
});
});
In this example, we’re expanding a section that starts with a height of 0
. We use scrollHeight
to grab the full height of the content and use that as an endpoint for the transition. After the transition completes, we switch the height to auto
, which allows the browser to automatically adjust the container's height based on its content. This step is optional, but it's useful if you expect the content inside the container to change over time.
New CSS features for animating display
and intrinsic size properties
Now let’s look at the new CSS features that have recently arrived or are on their way to browsers. These new tools eliminate the need for JavaScript in the scenarios we discussed earlier and help you write cleaner, shorter CSS.
Display and keyframes
The @keyframes
at-rule lets you create animations by controlling the intermediate steps in an animation sequence. The latest update allows you to animate the display
and [content-visibility](https://blog.logrocket.com/using-css-content-visibility-boost-rendering-performance/)
properties within a keyframe timeline.
We are not exactly interpolating between display none
and block
(because that’s not possible). Instead, we wait for all the other effects to complete and then switch the display state. This is similar to what we did with JavaScript — waiting for the transition to finish before applying display: none
— but now it’s much easier with CSS.
The Chrome Dev Blog has a really cool demo that makes things clear:
See the Pen Fade out cards - Animation by web.dev (@web-dot-dev) on CodePen.
First, the opacity is set to 0
over 250ms. Once this sequence is complete, the display is immediately set to none
:
.fade-out {
animation: fade-out 0.25s forwards;
}
/* Keyframe animations */
@keyframes fade-out {
100% {
opacity: 0;
display: none;
}
}
The biggest win here is that more complex animations involving the display
property, which were until recently super hard to implement using CSS (or JavaScript), can now be created relatively easily.
How transition-behavior
simplifies display
transitions
The fade-out effect can also now be created with a transition using the new [transition-behavior](https://developer.mozilla.org/en-US/docs/Web/CSS/transition-behavior)
property. This lets you apply transitions to properties that have a discrete animation behavior, like display
. By using allow-discrete
, you can animate the display
property. Here's a quick example:
.card {
transition: opacity 0.5s, display 0.5s;
transition-behavior: allow-discrete; /* this is essential */
}
.card.fade-out {
opacity: 0;
display: none;
}
Entry animations with @starting-style
We have discussed the fade-out effect quite extensively in this article. But what about the opposite case? Entry animations are tricky and often only possible through JavaScript. The new @starting-style
at-rule makes things much easier.
As the name suggests, we can use it to apply a style to an element that the browser can look up before the element is visible on the page. We can set the initial state of an entry animation here. Once the element renders, it transitions back to its default state.
Here’s a basic example:
.card {
@starting-style {
opacity: 0;
}
opacity: 1;
transition: opacity 0.5s;
}
The card will fade in once the DOM loads. You can use @starting-style
for all kinds of entry animations. Here’s another awesome example from the Chrome Dev team:
_See the Pen Item transitions by web.dev (@web-dot-dev) on CodePen.
_
Animating intrinsic sizes with the calc-size()
function
The calc-size
function, similar to calc()
, was recently introduced in Chrome 129. In simple words, it allows math to be performed on intrinsic sizes safely and reliably way. It currently supports operations of four keywords: auto
, min-content
, max-content
, and fit-content
.
This is especially useful for animating elements to and from their intrinsic size. calc-size
allows for animating any height that can currently be specified in CSS to zero or to/from a small fixed value. Here’s a simple example of expanding a collapsible section from height: 0
to auto
:
.card {
height: 0;
}
.card.open {
height: calc-size(auto);
}
Browser compatibility
Most of these features are primarily for enhancing animations and not essential DOM components, but because they are relatively new, it’s still worth checking browser compatibility:
- The
display
property is@keyframe
animatable in Chrome 116+ and Opera 102+. Firefox support is still in development, and Safari is working on it - The
[transition-behavior](https://caniuse.com/mdn-css_properties_transition-behavior)
property was released first in Chrome 117. It is compatible with all major browsers except Firefox, where it is currently in development - The
[@starting-style](https://caniuse.com/mdn-css_at-rules_starting-style)
at-rule was introduced in Chrome 117. It is fully supported on all major browsers besides Firefox, where it does not yet support animating fromdisplay: none
-
calc-size()
is the latest feature, introduced in Chrome 129, and is currently only supported in Chrome and Edge. However, other major browsers will soon support it
Conclusion
In this article, we explored the challenges developers face when animating CSS properties like display
and element sizes. Traditional methods required complex workarounds with CSS and JavaScript to achieve animations for properties that can’t be animated directly.
New features, such as animating display
with keyframes, the calc-size()
function, and the transition-behavior
property, make these animations easier to achieve. These functions eliminate the need for JavaScript, allowing for simpler CSS animations.
Is your frontend hogging your users' CPU?
As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If you’re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app, mobile app, or website. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.
Modernize how you debug web and mobile apps — start monitoring for free.
Top comments (0)