The Tools for Two-State CSS Animations

CSS animations have gained a lot of headline features lately, from scroll-driven effects to view transitions. But two smaller additions quietly solve one of the most persistent annoyances in everyday styling: animating elements in and out of display: none. The @starting-style rule and the transition-behavior: allow-discrete property finally give us native, declarative control over these cut-and-flip state changes.

The core limitation was described in the CSS Transitions Level 2 specification: transitions could only start on elements with an established "before-change style" from a previous style change event. An element that was hidden or newly created had no prior style, so there was nothing for the browser to animate from.

These two CSS features cover different parts of that problem. @starting-style defines the initial styles for an element right before it is rendered. transition-behavior: allow-discrete tells the browser to swap discrete property values — like display, visibility, and mix-blend-mode — at the halfway point of a transition rather than immediately at the start.

Stepping In and Out of the DOM

Consider a simple case: a list where buttons add and remove items via JavaScript. With the removal class applied, we can animate items exiting with opacity and transform declarations. But newly added items appear instantly. We need to tell the browser what their starting state should look like.

The @starting-style rule supplies that missing state. When an item is added to the DOM — or when it first becomes visible — the browser can now transition from the style block to the final declared style. With the rule set to zero opacity and a slight downward transform; entering items fade in from below. Change the starting style to a nudge from the top and items enter from above while still exiting downward.

This works when elements are actually created and destroyed. When we toggle visibility with the display property instead, the same technique applies, but there is a key difference. Applying display: none to a .hidden class still snaps the exit animation off instantly. Only the transition property alone cannot fix this.

We need to transition the display property itself and declare:

transition-behavior: allow-discrete;

That permits the browser to flip display at the midpoint of the transition instead of at frame zero, producing a smooth exit.

When using the transition shorthand, placement matters. If you declare the transition shorthand after transition-behavior, the shorthand resets it. So transition-behavior must come after the shorthand to keep its effect.

Elements of the Top Layer

Dialogs and popovers present a slightly different case because they live in the top layer — a rendering surface above the document flow. You can visualise it as a transparent sheet placed over your page; anything drawn on it appears above everything else, including high z-index values. Because the top layer is outside the normal document structure, it brings some useful properties:

  • No z-index dependency: Elements in the top layer always sit above the document regardless of any z-index.
  • Isolated stacking: Position in the DOM does not influence layering within the top layer.
  • Backdrop styling: A dedicated ::backdrop pseudo-element allows styling of the space between the top layer and the document underneath.

While a dialog or popover appears to be created on open and destroyed on close, it is actually being rendered into and out of the top layer. From the browser's perspective, it has nothing to transition from. @starting-style on the open attribute selector provides the entry state.

Moving Your Backdrop Smoothly

The ::backdrop pseudo-element works the same way but holds a subtle difference. Since a backdrop is a product of the top layer, its removal is controlled by the CSS overlay property, which specifies whether a top-layer element is currently being rendered. overlay is discrete, so it needs to be added alongside display in the transition-property declaration when animating dialogs and popovers. Do the same whether your opening and closing triggers come from JavaScript hooks or the newer invoker command attributes. Adding that extra property is only supported in Chromium at present, but graceful degradation makes it a worthwhile progressive enhancement.

Blending and Visibility: Other Discrete Properties

The visibility property is another discrete property that works well for transitions, particularly when you want an element to remain in the layout but become invisible. Replacing display: none with visibility: hidden preserves the space of the box while still allowing smooth transitions. The same demo from earlier, using visibility instead of display, demonstrates this behavior.

See the Pen [Transitioning the visibility property [forked]](https://codepen.io/smashingmag/pen/LEPMJqX) by utilitybend.

See the Pen Transitioning the visibility property [forked] by utilitybend.

Another discrete property is mix-blend-mode. While a practical use case is harder to identify, it is possible to transition between two blend modes. In the following example, the switch happens in the middle of the transition rather than immediately at the start.

See the Pen [Transitioning mix-blend-mode [forked]](https://codepen.io/smashingmag/pen/bNbOxZp) by utilitybend.

See the Pen Transitioning mix-blend-mode [forked] by utilitybend.

Transitioning Into the Top Layer

In a perfect world, transitioning elements in and out of the top layer would not require a dedicated property like transition-behavior. But given how CSS handles discrete properties, having this tool is a welcome addition to the platform.

The @starting-style rule is equally important here. It provides browsers with the initial styles an element should have at the beginning of a transition when it first enters the top layer. Without it, there is no initial state to transition from, making smooth entry and exit animations impossible for elements like modals or popovers.

Smashing Editorial