CSS @starting-style: Display None to Block Guide

The CSS @starting-style at-rule enables smooth entry transitions for elements changing their rendering state, specifically resolving the longstanding limitation of transitioning from display: none to display: block. This article explains how @starting-style defines an element's initial pre-render state, how it interacts with the transition engine, and the syntax required to animate entrance effects seamlessly without JavaScript workarounds.

The Problem with display: none

Historically, transitioning an element from display: none to display: block made CSS transitions fail. When an element has display: none, it generates no layout box and has no computed style tree.

Switching the element to display: block forces the browser to calculate its initial styles and final styles simultaneously in a single render frame. Because there was no previous frame or baseline style to interpolate from, the element would snap instantly to its visible state, ignoring any specified transition rules for properties like opacity or transform.

How @starting-style Solves the Issue

The @starting-style rule establishes a "before-open" or baseline state. It provides the browser with the computed values an element should have right before its first style update or at the exact moment its display changes from none to any rendered value (block, flex, grid, etc.).

When the browser detects a change from display: none to display: block:

  1. It applies the styles inside @starting-style as the initial snapshot.
  2. It immediately queues the target styles defined on the element.
  3. It interpolates between the @starting-style values and the element's actual styles over the declared transition duration.

The Role of transition-behavior: allow-discrete

While @starting-style defines the starting visual properties (like opacity: 0), transitioning display itself requires modern discrete animation handling.

By default, the display property cannot be transitioned. To allow an element to remain in the render tree (display: block) during both entrance and exit animations, you must transition the display property using transition-behavior: allow-discrete.

When combined:

  • On entering (display: block): The element switches to display: block immediately, applies @starting-style values, and transitions to the target properties.
  • On exiting (display: none): The element transitions its visual properties first, delaying the switch to display: none until the animation ends.

Implementation Example

.card {
  /* Final rendered state */
  display: block;
  opacity: 1;
  transform: translateY(0);

  /* Enable transitions on both standard and discrete properties */
  transition: 
    opacity 0.4s ease, 
    transform 0.4s ease, 
    display 0.4s allow-discrete;
}

/* Initial state when switching away from display: none */
@starting-style {
  .card {
    opacity: 0;
    transform: translateY(-20px);
  }
}

/* Hidden state */
.card.is-hidden {
  display: none;
  opacity: 0;
  transform: translateY(-20px);
}

Nesting @starting-style

@starting-style can also be nested directly within the component selector using modern CSS nesting rules:

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

  @starting-style {
    opacity: 0;
    transform: scale(0.9);
  }

  &.is-closed {
    display: none;
    opacity: 0;
    transform: scale(0.9);
  }
}

Key Rules to Remember

  • First Render Only: @starting-style only takes effect when an element is first rendered to the DOM or when its display toggles out of none. It has no impact on normal property updates once the element is already rendered.
  • Pair with Target Styles: The properties declared inside @starting-style must match the properties being animated on the target element.
  • Discrete Transitions: Always include display in your transition-property declaration with allow-discrete to ensure entrance and exit cycles operate symmetrically without snapping.