CSS @starting-style for Entry Transitions

The CSS @starting-style at-rule solves a long-standing limitation in web development by enabling smooth entry transitions for elements appearing on a page, entering the top layer, or toggling from display: none. Historically, CSS transitions required an element to already exist in the DOM with an established style state, making it impossible to animate an element immediately upon mounting or display toggling without JavaScript timing hacks. This article explores the mechanics of @starting-style, its role in entry and exit lifecycles, and how it pairs with discrete property transitions to create seamless UI animations.

The Problem: Animating Initial Rendering

CSS transitions work by interpolating between two distinct states over a duration. When an element is added to the DOM or changed from display: none to another display value (such as block or flex), the browser evaluates its initial styles and renders them instantly. Because there is no previously rendered frame to compare against, standard transition rules fail to trigger, resulting in an abrupt visual pop rather than a fluid animation.

Developers traditionally resolved this limitation using JavaScript by rendering the element hidden, forcing a reflow or waiting for an animation frame, and then adding an active class to trigger the transition.

How @starting-style Works

The @starting-style at-rule provides a native, declarative way to define the element's style state before its first style update. When the browser computes the styles for an element that is becoming visible, it reads the properties within @starting-style as the base state, immediately interpolating them toward the element’s standard styles.

/* Base state of the element when visible */
.dialog {
  opacity: 1;
  transform: scale(1);
  transition: opacity 0.3s ease, transform 0.3s ease;
}

/* Starting state applied before initial render */
@starting-style {
  .dialog {
    opacity: 0;
    transform: scale(0.9);
  }
}

In this example, when .dialog is inserted into the document, the browser assigns it an opacity of 0 and a scale of 0.9 before animating it to an opacity of 1 and a scale of 1.

Pairing with Discrete Transitions for Display Changes

While @starting-style defines the starting values for properties like opacity or transform, transitioning from display: none requires an additional property: transition-behavior: allow-discrete.

By default, discrete properties like display switch values instantly at the start or end of an animation. By permitting discrete transitions, you instruct the browser to keep the element displayed until continuous transitions (like fading out) finish:

.card {
  display: block;
  opacity: 1;
  transition: opacity 0.3s ease, display 0.3s ease;
  transition-behavior: allow-discrete;
}

.card.hidden {
  display: none;
  opacity: 0;
}

@starting-style {
  .card {
    opacity: 0;
  }
}

Entry vs. Exit Transitions

It is important to note the specific boundary of @starting-style:

  • Entry Transitions: @starting-style applies only to the entry phase. It defines the state from which an element transitions when moving from not rendered to rendered.
  • Exit Transitions: Exit transitions do not use @starting-style. Instead, the exit state is handled by standard class updates (such as an .is-closing class) or pseudo-classes while transition-behavior: allow-discrete ensures the element remains visible throughout the closing animation duration.

Top-Layer Elements and Popovers

The @starting-style rule plays a critical role in native HTML elements such as <dialog> and elements leveraging the Popover API. When a dialog opens using .showModal(), it enters the browser's top layer. Combining @starting-style with the overlay property allows dialog backdrops and popovers to smoothly fade into view and dismiss without relying on complex third-party animation libraries.