![Una Kravets](https://web.dev/images/authors/unakravets.jpg) Una Kravets [X](https://twitter.com/una) [Mastodon](https://front-end.social/@una) [Homepage](https://una.im) ![Joey Arhar](https://web.dev/images/authors/jarhar.jpg) Joey Arhar [X](https://twitter.com/josepharhar) [GitHub](https://github.com/josepharhar)

<br />

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` and `content-visibility` on a keyframe timeline (From Chrome 116).
- The `transition-behavior` property with the `allow-discrete` keyword to enable transitions of discrete properties like `display` (From Chrome 117).
- The `@starting-style` rule to animate entry effects from `display: 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;
      }
    }

<iframe allow="camera; clipboard-read; clipboard-write; encrypted-media; geolocation; microphone; midi;" loading="lazy" src="https://codepen.io/web-dot-dev/embed/qBQeojK?height=500&amp;theme-id=light&amp;default-tab=result&amp;editable=true" style="height: 100%; width: 100%; border: 0;" title="Pen qBQeojK by web-dot-dev on Codepen">See the Pen &amp;amp;lt;a href="https://codepen.io/web-dot-dev/embed/qBQeojK"&amp;amp;gt;Pen qBQeojK by web-dot-dev on Codepen&amp;amp;lt;/a&amp;amp;gt;</iframe>

> [!NOTE]
> **Note:** Other discrete properties switch states at the 50% mark of the transition's easing function over the course of its duration. However, `display` and `content-visibility` switch at the beginning of the transition (if you are animating in) or end of the transition (if you are animating out).

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](https://developer.chrome.com/blog/entry-exit-animations#transitioning-discrete-animations)). 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');
    })

<iframe allow="camera; clipboard-read; clipboard-write; encrypted-media; geolocation; microphone; midi;" loading="lazy" src="https://codepen.io/web-dot-dev/embed/GRwVxEa?height=500&amp;theme-id=light&amp;default-tab=result&amp;editable=true" style="height: 100%; width: 100%; border: 0;" title="Pen GRwVxEa by web-dot-dev on Codepen">See the Pen &amp;amp;lt;a href="https://codepen.io/web-dot-dev/embed/GRwVxEa"&amp;amp;gt;Pen GRwVxEa by web-dot-dev on Codepen&amp;amp;lt;/a&amp;amp;gt;</iframe>

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;
    }

<iframe allow="camera; clipboard-read; clipboard-write; encrypted-media; geolocation; microphone; midi;" loading="lazy" src="https://codepen.io/web-dot-dev/embed/poQMLrN?height=500&amp;theme-id=light&amp;default-tab=result&amp;editable=true" style="height: 100%; width: 100%; border: 0;" title="Pen poQMLrN by web-dot-dev on Codepen">See the Pen &amp;amp;lt;a href="https://codepen.io/web-dot-dev/embed/poQMLrN"&amp;amp;gt;Pen poQMLrN by web-dot-dev on Codepen&amp;amp;lt;/a&amp;amp;gt;</iframe> Note: This transition demo shows a different technique than the first animation demo but visually looks similar.

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 */
    }

> [!NOTE]
> **Important:** When no `transition-behavior` is set in the `transition` shorthand it uses the initial value of `transition-behavior`, which is `normal`. Therefore you must declare `transition-behavior: allow-discrete` *after* the shorthand, because otherwise the shorthand reverts the value back to `normal`.

### 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);
    }

> [!IMPORTANT]
> **Important:** The rules inside `@starting-style` cascade as any other grouped style rules without introducing any new ordering to the cascade, which means declarations and rules inside `@starting-style` do not necessarily win over those outside. To ensure that starting styles don't get overwritten, place the `@starting-style` rule after the non-starting-style declarations.

Now you have both an entry and exit state for these TODO list items:
<iframe allow="camera; clipboard-read; clipboard-write; encrypted-media; geolocation; microphone; midi;" loading="lazy" src="https://codepen.io/web-dot-dev/embed/xxQvWLW?height=500&amp;theme-id=light&amp;default-tab=result&amp;editable=true" style="height: 100%; width: 100%; border: 0;" title="Pen xxQvWLW by web-dot-dev on Codepen">See the Pen &amp;amp;lt;a href="https://codepen.io/web-dot-dev/embed/xxQvWLW"&amp;amp;gt;Pen xxQvWLW by web-dot-dev on Codepen&amp;amp;lt;/a&amp;amp;gt;</iframe>

## 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;
    }

<iframe allow="camera; clipboard-read; clipboard-write; encrypted-media; geolocation; microphone; midi;" loading="lazy" src="https://codepen.io/web-dot-dev/embed/dyQxmzg?height=500&amp;theme-id=light&amp;default-tab=result&amp;editable=true" style="height: 100%; width: 100%; border: 0;" title="Pen dyQxmzg by web-dot-dev on Codepen">See the Pen &amp;amp;lt;a href="https://codepen.io/web-dot-dev/embed/dyQxmzg"&amp;amp;gt;Pen dyQxmzg by web-dot-dev on Codepen&amp;amp;lt;/a&amp;amp;gt;</iframe>

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;
    }

<iframe allow="camera; clipboard-read; clipboard-write; encrypted-media; geolocation; microphone; midi;" loading="lazy" src="https://codepen.io/web-dot-dev/embed/RwqXMZd?height=650&amp;theme-id=light&amp;default-tab=result&amp;editable=true" style="height: 100%; width: 100%; border: 0;" title="Pen RwqXMZd by web-dot-dev on Codepen">See the Pen &amp;amp;lt;a href="https://codepen.io/web-dot-dev/embed/RwqXMZd"&amp;amp;gt;Pen RwqXMZd by web-dot-dev on Codepen&amp;amp;lt;/a&amp;amp;gt;</iframe>

### `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;
    }

<iframe allow="camera; clipboard-read; clipboard-write; encrypted-media; geolocation; microphone; midi;" loading="lazy" src="https://codepen.io/web-dot-dev/embed/NWEQYaG?height=500&amp;theme-id=light&amp;default-tab=result&amp;editable=true" style="height: 100%; width: 100%; border: 0;" title="Pen NWEQYaG by web-dot-dev on Codepen">See the Pen &amp;amp;lt;a href="https://codepen.io/web-dot-dev/embed/NWEQYaG"&amp;amp;gt;Pen NWEQYaG by web-dot-dev on Codepen&amp;amp;lt;/a&amp;amp;gt;</iframe>

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](https://jsfiddle.net/jarhar/r6a5bvnt/). 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](https://developer.chrome.com/docs/web-platform/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.
<iframe allow="camera; clipboard-read; clipboard-write; encrypted-media; geolocation; microphone; midi;" loading="lazy" src="https://codepen.io/web-dot-dev/embed/JjwPPgy?height=500&amp;theme-id=light&amp;default-tab=result&amp;editable=true" style="height: 100%; width: 100%; border: 0;" title="Pen JjwPPgy by web-dot-dev on Codepen">See the Pen &amp;amp;lt;a href="https://codepen.io/web-dot-dev/embed/JjwPPgy"&amp;amp;gt;Pen JjwPPgy by web-dot-dev on Codepen&amp;amp;lt;/a&amp;amp;gt;</iframe>

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.
<iframe allow="camera; clipboard-read; clipboard-write; encrypted-media; geolocation; microphone; midi;" loading="lazy" src="https://codepen.io/web-dot-dev/embed/qBLWBOV?height=500&amp;theme-id=light&amp;default-tab=result&amp;editable=true" style="height: 100%; width: 100%; border: 0;" title="Pen qBLWBOV by web-dot-dev on Codepen">See the Pen &amp;amp;lt;a href="https://codepen.io/web-dot-dev/embed/qBLWBOV"&amp;amp;gt;Pen qBLWBOV by web-dot-dev on Codepen&amp;amp;lt;/a&amp;amp;gt;</iframe>

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

- [Introducing the popover API](https://developer.chrome.com/blog/introducing-popover-api)
- [css-transitions-2 spec](https://drafts.csswg.org/css-transitions-2/#defining-before-change-style-the-starting-style-rule)
- [css-position-4 spec](https://drafts.csswg.org/css-position-4/#overlay)

> [!NOTE]
> **Note:** This post was updated on 2024-09-13 to use the correct order for nested `@starting-style` rules. As of Chrome 130, `@starting-style` rules must be placed after the post-entry state (normal) declarations.