Enter and exit animations using @starting-style and allow-discrete

Published at: 15 May 2025

Exit and enter animations have historically been annoying to implement. Frameworks have made it easier, but what if you just want to use vanilla css? @starting-style is here to solve your problems! @starting-style is available in all modern browsers, some of the of the other css features aren't available in firefox yet, but I'll mention that when we get to it. Let's go!

The goal

We'll be using this popover as an example, but this works for any element that enters and leaves the dom (like an element with display:none;)

I'm popping off!

And here's the code if you're in a hurry:

.popover {
  /* Exit styles */
  opacity: 0;
  transform: translateY(100%);
  transition:
    opacity 1s ease,
    transform 1s ease,
    display 1s allow-discrete;
  /* allow-discrete is not supported in firefox as of writing */
}

.popover:popover-open {
  /* Default styles */
  transform: translateY(0);
  opacity: 1;
}

@starting-style {
  /* Starting/enter styles */
  .popover:popover-open {
    transform: translateY(-100%);
    opacity: 0;
  }
}

The explanation

We'll be using this as a starting point:

I'm popping off!

Starting animations

Previously, there was no way to transition from display: none to a visible element. A hidden element still has the same styles as when it's visible, so there is nothing to transition from! @starting-style allows you to define styles for an element just before it's rendered in the dom. This way, the browser has something to use as a starting point.

It looks like this:

@starting-style {
  your-element {
    <style-definition>
  }
}

Where your-element is the selector to select the element you want to animate, and <style-definition> is where you define your starting styles, simple! Now, if you add a transition, it can transition between the two values:

.popover {
  transition:
    opacity 1s ease,
    transform 1s ease;
}

.popover:popover-open {
  transform: translateY(0);
  opacity: 1;
}

@starting-style {
  /* Starting/enter styles */
  .popover:popover-open {
    transform: translateY(-100%);
    opacity: 0;
  }
}
I'm popping off!

Magic! It applies the transition when entering! Because of the @starting-style, the browser knows what to use as the starting point, allowing us to animate the opacity and transform. (Notice: we're setting @starting-style on the :popover-open pseudoclass because we're using a popover. If this was a regular element you could set it on the basic class/styles)

Exit animations

Enter animations are already a very useful tool to have, but they're only half of the puzzle. Let's look at how to define exit animations as well. Previously, it wasn't possible to animate properties like display. The values it accepts aren't like numbers, or colours. You can't map a point halfway between none and block; They're separate, discrete values.

This property isn't available in firefox as of writing

Recently, the animation-behavior property became available in most major browsers (except firefox 😔), and with it came allow-discrete value. You can use it both as a standalone property, or as part of a transition definition:

.toggle-me {
  transition:
    opacity 1s ease,
    display 1s allow-discrete;
}
/* Or: */
.toggle-me {
  transition:
    opacity 1s ease,
    display 1s;
  transition-behavior: allow-discrete;
}

When setting this property on a transition for a discrete property, it allows supporting browsers to animate it. Now, instead of instantly flipping the value, it will flip halfway through the transition. It will do this for all discrete values, except display.

When using it with display, the browser will instead make sure that the element it's set on will be visible during the whole transition. This allows you to define exit animations! Let's add it to our popover:

.popover {
  /* Exit styles */
  transition:
    opacity 1s ease,
    transform 1s ease,
    display 1s allow-discrete;
  /* allow-discrete is not supported in firefox as of writing */
}

.popover:popover-open {
  /*...*/
}

@starting-style {
  /*...*/
}
I'm popping off!

Notice how it's taking a while to disappear now? Because we added a transition for the display property, and added allow-discrete, the browser will wait until the transition is done to actually set the property. If we then also add an opacity of 0 to the hidden state, we can also fade it out:

.popover {
  /* Exit styles */
  opacity: 0;
  transform: translateY(100%);
  transition:
    opacity 1s ease,
    transform 1s ease,
    display 1s allow-discrete;
  /* allow-discrete is not supported in firefox as of writing */
}

.popover:popover-open {
  /* Default styles */
  transform: translateY(0);
  opacity: 1;
}

@starting-style {
  /* Starting/enter styles */
  .popover:popover-open {
    transform: translateY(-100%);
    opacity: 0;
  }
}
I'm popping off!

That's all there is to it!

Browser support

As of writing, @starting-style is only supported in around 88% of modern browsers. allow-discrete has slightly worse support, with around 85% support. Those are not great stats. However, the functionalities these properties add are mostly cosmetic (when used that way). This means they're great candidates for progressive enhancement! You can add them now as a bonus for supporting browsers, and once the other browsers catch up, they'll automatically use them too!

Accessibility

As with all big transitions, don't forget to add @media (prefers-reduced-motion) around the transitions! I haven't added them in the examples for brevity, but you should!

Conclusion

Using @starting-style and allow-discrete together allows you to create smoother experiences for your users. They can be a little tricky to wrap your head around when you first encounter them. But I find it's easiest to build up the animation slowly, bit by bit. Start with the enter animation, then move on to exit. Hopefully this post helped you out a bit, and you'll make some fun stuff with it. Send me a message if you want to show it off!