DEV Community

Cover image for Simplifying CSS animations with the display and size properties
Megan Lee for LogRocket

Posted on • Originally published at blog.logrocket.com

Simplifying CSS animations with the display and size properties

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
} 
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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";
  });
});
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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 from display: 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 Signup

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)