CSS @starting-style: Animate Elements Entering the DOM

The CSS @starting-style rule provides a native mechanism to animate elements as they are newly inserted into the Document Object Model (DOM) or toggled from display: none. Historically, CSS transitions required an already-rendered starting state to interpolate values, forcing developers to rely on JavaScript reflow hacks or @keyframes. This article explains how @starting-style defines an initial pre-render style state, how the browser processes it during DOM entry, and how to configure transitions for smooth mount and unmount animations.

The Problem with Initial Mount Transitions

CSS transitions are designed to animate property changes between two computed states over time. However, when an element is dynamically inserted into the DOM or changes from display: none to a visible display state (such as block or flex), the browser does not have a previous rendered frame to transition from.

Without a baseline, the browser instantly computes the element's final styles on the first paint, causing the element to appear abruptly without executing any defined CSS transition. In the past, bypassing this limitation required forcing a browser reflow using JavaScript (such as accessing element.offsetHeight) or using CSS @keyframes animations, which are more rigid and harder to reverse cleanly.

How @starting-style Functions

The @starting-style at-rule establishes initial style values for an element before its first style resolution and paint cycle.

When an element becomes visible, the browser checks if a @starting-style block applies to it. If present, the engine uses these properties as the starting point (from-values) and immediately transitions them to the element's standard styles (to-values) according to its transition properties.

Under the hood, the process works in three steps:

  1. Instantiation: The element is appended to the DOM or changes from display: none.
  2. Initial Resolution: The browser assigns the property values declared inside @starting-style as the initial computed styles before painting.
  3. Transition Execution: The browser switches the element's styles to its regular rule set, triggering the standard CSS transition pipeline toward the final values.

Basic Syntax and Implementation

@starting-style can be written as a standalone rule targeting a selector or nested directly inside a CSS rule block.

.card {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.4s ease, transform 0.4s ease;
}

/* Define the entry state */
@starting-style {
  .card {
    opacity: 0;
    transform: translateY(20px);
  }
}

When .card is injected into the DOM via JavaScript, it instantly initializes at opacity: 0 and translateY(20px), then smoothly animates to opacity: 1 and translateY(0).

Animating from display: none

When animating an element toggled via the display property, @starting-style must be paired with transition-behavior: allow-discrete (or the allow-discrete keyword in the transition shorthand). Because display is a discrete property, this flag ensures the element remains rendered as display: block for the duration of the transition rather than snapping immediately.

.modal {
  display: flex;
  opacity: 1;
  transform: scale(1);
  transition: opacity 0.3s ease, transform 0.3s ease, display 0.3s allow-discrete;
}

.modal.is-hidden {
  display: none;
  opacity: 0;
  transform: scale(0.95);
}

@starting-style {
  .modal {
    opacity: 0;
    transform: scale(0.95);
  }
}

Animating Native Top-Layer Elements

@starting-style seamlessly handles native browser-managed UI, such as <dialog> elements and components utilizing the Popover API. Because these elements toggle between open and closed states, you can define their entry styles directly alongside their open states:

dialog {
  opacity: 0;
  transform: scale(0.8);
  transition: opacity 0.3s ease, transform 0.3s ease, display 0.3s allow-discrete, overlay 0.3s allow-discrete;
}

dialog[open] {
  opacity: 1;
  transform: scale(1);
}

/* Entry state when the dialog is opened */
@starting-style {
  dialog[open] {
    opacity: 0;
    transform: scale(0.8);
  }
}

In this setup, the dialog element smoothly scales and fades in upon calling dialog.showModal(), while the overlay transition keyword ensures proper rendering timing within the browser's top-layer stack.