Motion is a core part of any digital experience, guiding your user from one interaction to the next. But there are a few gaps in smooth animations on the web platform. These include the ability to easily animate entry and exit animations, and smoothly animate to and from the top layer for dismissible elements such as dialogs and popovers.
To fill these gaps, Chrome 116 and 117 includes four new web platform features, which enable smooth animations and transitions for discrete properties.
These four new features include:
- The ability to animate
display
andcontent-visibility
on a keyframe timeline (From Chrome 116). - The
transition-behavior
property with theallow-discrete
keyword to enable transitions of discrete properties likedisplay
(From Chrome 117). - The
@starting-style
rule to animate entry effects fromdisplay: none
and into the top-layer (From Chrome 117). - The
overlay
property to control top-layer behavior during an animation (From Chrome 117).
Display animations in keyframes
From Chrome 116, you can use display
and content-visibility
in keyframe rules. These will then swap at the time the keyframe occurs. No additional new values are required to support this:
.card {
animation: fade-out 0.5s forwards;
}
@keyframes fade-out {
100% {
opacity: 0;
display: none;
}
}
The preceding example animates the opacity to 0 over the 0.5s duration and then set display to none. Additionally, the forwards
keyword ensures that the animation remains at its end state, so that the element it is applied to remains display: none
and opacity: 0
.
This is a simple example that mimics what you can do with a transition (see demo in transition section). Transitions, however, are unable to create more complex animations, such as the following example:
.card {
animation: spin-and-delete 1s ease-in forwards;
}
@keyframes spin-and-delete {
0% {
transform: rotateY(0);
filter: hue-rotate(0);
}
80% {
transform: rotateY(360deg);
filter: hue-rotate(180deg);
opacity: 1;
}
100% {
opacity: 0;
display: none;
}
}
The spin-and-delete
animation is an exit animation. First, the card will spin on the y-axis, run through a hue-rotation, and then at 80%
through the timeline, transitions its opacity from 1 to 0. Finally, the card swaps from display: block
to display: none
.
For these exit animations, instead of applying them directly to an element, you can set up a trigger for the animations. For example by attaching an event listener to a button that triggers a class to apply the animation, like so:
.spin-out {
animation: spin-and-delete 1s ease-in forwards;
}
document.querySelector('.delete-btn').addEventListener('click', () => {
document.querySelector('.card').classList.add('spin-out');
})
The example above now has an end-state of display:none
. There are many cases where you’ll want to take it further and remove the DOM node with a timeout to allow for the animation to finish first.
Transitioning discrete properties
Properties that animate discretely don't trigger transitions events by default. To enable this, set the transition behavior mode to allow-discrete
.
The transition-behavior
property
The transition-behavior
property specifies whether transitions will be started or not for discrete properties. It accepts two values: normal
and allow-discrete
, with the initial value being normal
.
normal
: Transitions will not be started for discrete properties, only for interpolable properties.allow-discrete
: Transitions will be started for discrete properties as well as interpolable properties.
To enable allow-discrete
mode for a specific property, include it in the transition
shorthand:
.card {
transition: opacity 0.25s, display 0.25s allow-discrete; /* Enable allow-discrete for the display property */
}
.card.fade-out {
opacity: 0;
display: none;
}
When transitioning multiple discrete properties, you must set allow-discrete
for each transitioning property. For example:
.card {
transition: opacity 0.5s, display 0.5s allow-discrete, overlay 0.5s allow-discrete;
}
Alternatively, to set the behavior for all transitioning properties, declare transition-behavior: allow-discrete
after the transition
declaration. This is often the easiest approach.
.card {
transition: opacity 0.5s, display 0.5s, overlay 0.5s;
transition-behavior: allow-discrete; /* Note: be sure to write this after the shorthand */
}
The @starting-style
rule for entry animations
So far, this article has covered exit animations, to create entry animations you need to use the @starting-style
rule.
Use @starting-style
to apply a style that the browser can look up before the element is open on the page. This is the “before-open” state (where you are animating in from).
/* 0. IS-OPEN STATE */
/* The state at which the element is open + transition logic */
.item {
height: 3rem;
display: grid;
overflow: hidden;
transition: opacity 0.5s, transform 0.5s, height 0.5s, display 0.5s allow-discrete;
}
/* 1. BEFORE-OPEN STATE */
/* Starting point for the transition */
@starting-style {
.item {
opacity: 0;
height: 0;
}
}
/* 2. EXITING STATE */
/* While it is deleting, before DOM removal in JS, apply this
transformation for height, opacity, and a transform which
skews the element and moves it to the left before setting
it to display: none */
.is-deleting {
opacity: 0;
height: 0;
display: none;
transform: skewX(50deg) translateX(-25vw);
}
Now you have both an entry and exit state for these TODO list items:
Animating elements to and from the top-layer
To animate elements to and from the top-layer, specify the @starting-style
on the “open” state to tell the browser where to animate in from. For a dialog, the open state is defined with the [open]
attribute. For a popover, use the :popover-open
pseudo class.
A simple example of a dialog could look like this:
/* 0. IS-OPEN STATE */
dialog[open] {
translate: 0 0;
}
/* 1. BEFORE-OPEN STATE */
@starting-style {
dialog[open] {
translate: 0 100vh;
}
}
/* 2. EXIT STATE */
dialog {
transition: translate 0.7s ease-out, overlay 0.7s ease-out allow-discrete, display 0.7s ease-out allow-discrete;
translate: 0 100vh;
}
In the next example, the entry and exit effects are different. Enter by animating up from the bottom of the viewport, exit the effect to the top of the viewport. It is also written with nested CSS for more visual encapsulation.
When animating a popover, use the :popover-open
pseudo class instead of the open
attribute used previously.
.settings-popover {
&:popover-open {
/* 0. IS-OPEN STATE */
/* state when popover is open, BOTH:
what we're transitioning *in* to
and transitioning *out* from */
transform: translateY(0);
opacity: 1;
/* 1. BEFORE-OPEN STATE */
/* Initial state for what we're animating *in* from,
in this case: goes from lower (y + 20px) to center */
@starting-style {
transform: translateY(20px);
opacity: 0;
}
}
/* 2. EXIT STATE */
/* Initial state for what we're animating *out* to ,
in this case: goes from center to (y - 50px) higher */
transform: translateY(-50px);
opacity: 0;
/* Enumerate transitioning properties,
including display and allow-discrete mode */
transition: transform 0.5s, opacity 0.5s, display 0.5s allow-discrete;
}
overlay
property
Finally, to fade out a popover
or dialog
from the top layer, add the overlay
property to your list of transitions. popover
and dialog
escape ancestor clips and transforms, and also put the content in the top layer. If you don't transition overlay
, your element will immediately go back to being clipped, transformed, and covered up, and you won't see the transition happen.
[open] {
transition: opacity 1s, display 1s allow-discrete;
}
Instead, include overlay
in the transition or animation to animate overlay
along with the rest of the features and ensure it stays in the top layer when animating. This will look much smoother.
[open] {
transition: opacity 1s, display 1s allow-discrete, overlay 1s allow-discrete;
}
Additionally, when you have multiple elements open in the top-layer, overlay helps you control the smooth transition in and out of the top layer. You can see the difference in this simple example. If you are not applying overlay
to the second popover when transitioning it out, it will first move out of the top layer, jumping behind the other popover, before starting the transition. This isn’t a very smooth effect.
A note on view transitions
If you are making DOM changes, such as adding and removing elements from the DOM, another great solution for smooth animations is view transitions. Here are two of the above examples built using view transitions.
In this first demo, instead of setting up @starting-style
and other CSS transforms, view transitions will handle the transition. The view transition is set up like so:
First, in CSS, give each card an individual view-transition-name
.
.card-1 {
view-transition-name: card-1;
}
.card-2 {
view-transition-name: card-2;
}
/* etc. */
Then, in JavaScript, wrap the DOM mutation (in this case, removing the card), in a view transition.
deleteBtn.addEventListener('click', () => {
// Check for browser support
if (document.startViewTransition) {
document.startViewTransition(() => {
// DOM mutation
card.remove();
});
}
// Alternative if no browser support
else {
card.remove();
}
})
Now, the browser can handle the fade out and morph of each card to its new position.
Another example of where this can be handy is with the add/remove list items demo. In this case, you'll need to remember to add a unique view-transition-name
for each card created.
Conclusion
These new platform features bring us one step closer to smooth entry and exit animations on the web platform. To learn more, check out these links: