PureDevTools

CSS @starting-style Reference

First-render transitions, display:none entry states, and popover/dialog patterns with ready-to-copy @starting-style CSS

All processing happens in your browser. No data is sent to any server.

CSS selector for the element to animate — e.g. .toast, .drawer

Animated Properties

Start opacity:→ end: 1
Start transform:→ end: none

Transition

Animated via @starting-style (2 properties):

  • opacity
  • transform

Generated CSS

/* Entry animation — .notification */
.notification {
  /* End state (element is visible) */
  opacity: 1;
  transform: none;
  transition:
    opacity 0.3s ease,
    transform 0.3s ease;
}

@starting-style {
  .notification {
    /* Transition start values — element enters FROM these styles */
    opacity: 0;
    transform: translateY(-8px);
  }
}

You add a CSS transition to a notification banner — opacity 0.3s ease. You test it: the banner appears, but there is no fade-in. It just pops in at full opacity. The transition only fires when the element changes state, not when it first appears. @starting-style exists for exactly this case: it gives the browser a synthetic “before” state for an element that did not previously exist or was display: none.

Quick Answers

Why This Reference (Not MDN)

MDN’s @starting-style documentation covers the syntax but it does not lead with the question most people actually search for: “when do I need @starting-style instead of a normal transition?” This guide starts with that answer, then covers popovers, dialogs, display: none, allow-discrete, and the browser-support caveats that usually trip people up.

What Is the CSS @starting-style At-Rule?

The CSS @starting-style at-rule solves a fundamental problem with CSS transitions: transitions do not fire when an element is first rendered because there is no “before” state to transition from. @starting-style provides that missing initial state, enabling smooth entry animations for elements entering the page.

/* Without @starting-style — element appears instantly, no transition */
.notification {
  opacity: 1;
  transition: opacity 0.3s ease;
}

/* With @starting-style — element transitions from 0 → 1 */
.notification {
  opacity: 1;
  transition: opacity 0.3s ease;
}

@starting-style {
  .notification {
    opacity: 0; /* browser transitions FROM this value */
  }
}

@starting-style shipped in Chrome 117+, Edge 117+, Firefox 129+, and Safari 17.5+ — covering the vast majority of modern browsers.

How @starting-style Works

When a CSS transition is computed, the browser needs both a “before” value and an “after” value. For elements that already exist in the DOM, the before value comes from the element’s previous computed style. For elements that are new — just inserted into the DOM or just becoming visible from display: none — there is no previous computed style.

@starting-style provides the “before” values for these cases:

  1. First render: element is added to the DOM for the first time
  2. display: none → visible: element transitions from hidden to shown
  3. Popover open: [popover] element enters the :popover-open state
  4. Dialog open: dialog element enters the [open] state

The browser transitions FROM the @starting-style values TO the element’s regular computed styles.

Basic @starting-style Usage

/* Regular styles — the "end state" of the transition */
.card {
  opacity: 1;
  transform: translateY(0);
  transition:
    opacity 0.4s ease,
    transform 0.4s ease;
}

/* @starting-style — the "start state" of the transition */
@starting-style {
  .card {
    opacity: 0;
    transform: translateY(-12px);
  }
}

When .card is added to the DOM, the browser reads the @starting-style values as the start, computes opacity: 1; transform: translateY(0) as the end, and runs the transition.

When NOT to Use @starting-style

@starting-style is not a replacement for ordinary CSS transitions. If an element is already on the page and you are toggling a class, :hover, :focus, [open], or some other state where the browser already knows the previous computed style, a normal transition is enough.

Use a regular transition for state changes like this:

.button {
  background: #111827;
  transition: background-color 0.2s ease;
}

.button:hover {
  background: #1f2937;
}

Use @starting-style only when the browser would otherwise have no “before” value to animate from.

Animating from display: none

Toggling display: none normally prevents transitions because the property switches discretely (instantly). To animate across the display: none boundary, you need two things:

  1. display Xs allow-discrete in the transition shorthand — enables transition-behavior: allow-discrete for the display property
  2. @starting-style for the entry starting state
.drawer {
  display: block;
  opacity: 1;
  transform: translateX(0);
  transition:
    opacity 0.3s ease,
    transform 0.3s ease,
    display 0.3s allow-discrete; /* enables discrete display toggling */
}

.drawer.is-closed {
  display: none;
  opacity: 0;
  transform: translateX(-100%);
}

/* Entry animation — fires when .is-closed is removed */
@starting-style {
  .drawer {
    opacity: 0;
    transform: translateX(-100%);
  }
}

The allow-discrete keyword is equivalent to transition-behavior: allow-discrete applied to a specific property in the transition shorthand.

Popover API Animations

The Popover API moves elements into and out of the CSS top layer. Animating popover entry and exit requires three components:

  1. @starting-style — entry start values when the popover opens
  2. display allow-discrete — allows the display property to transition
  3. overlay allow-discrete — keeps the element in the top layer during the closing animation
/* Exit / closed state */
[popover] {
  opacity: 0;
  transform: scale(0.95);
  transition:
    opacity 0.25s ease,
    transform 0.25s ease,
    display 0.25s allow-discrete,
    overlay 0.25s allow-discrete;
}

/* Open state */
[popover]:popover-open {
  opacity: 1;
  transform: scale(1);
}

/* Entry start state */
@starting-style {
  [popover]:popover-open {
    opacity: 0;
    transform: scale(0.95);
  }
}

Why overlay allow-discrete?

Without overlay allow-discrete, the popover exits the top layer immediately when it closes, making it invisible before the closing animation can run. overlay allow-discrete delays the top-layer exit until after the transition completes.

Dialog Element Animations

Dialog elements also use the top layer when opened via showModal(). The pattern is identical to popover animations, using dialog[open] as the open-state selector:

dialog {
  opacity: 0;
  transform: scale(0.95) translateY(8px);
  transition:
    opacity 0.2s ease,
    transform 0.2s ease,
    display 0.2s allow-discrete,
    overlay 0.2s allow-discrete;
}

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

@starting-style {
  dialog[open] {
    opacity: 0;
    transform: scale(0.95) translateY(8px);
  }
}

Animating the Dialog Backdrop

The ::backdrop pseudo-element can also use @starting-style:

dialog::backdrop {
  background-color: rgb(0 0 0 / 0.5);
  transition:
    background-color 0.2s ease,
    display 0.2s allow-discrete,
    overlay 0.2s allow-discrete;
}

@starting-style {
  dialog::backdrop {
    background-color: rgb(0 0 0 / 0);
  }
}

The overlay Property

The overlay property is a special CSS property that controls whether an element participates in the CSS top layer. It is automatically applied by the browser to top-layer elements (popovers, dialogs, fullscreen elements). Developers cannot set it directly — only allow-discrete can be applied to delay its removal.

Featuredisplay allow-discreteoverlay allow-discrete
What it doesEnables transitions across display: noneDelays top-layer exit until transition ends
Required fordisplay: none animationsPopover/dialog closing animations
Safari support17.5+Not yet

@starting-style vs. CSS Animations

Feature@starting-styleCSS @keyframes
TriggerElement appearingAnimation property set
Syntax@starting-style { selector { } }@keyframes name { from { } to { } }
EasingTransition easingAnimation timing
LoopingOne-time entryConfigurable
display: noneWith allow-discreteRequires JS hack
ComplexitySimpleMore verbose

@starting-style is best for entry transitions — a single animation when an element first appears. @keyframes is better for repeated, looping, or complex multi-step animations.

Browser Support

Browser@starting-styledisplay allow-discreteoverlay allow-discrete
Chrome117+117+117+
Firefox129+129+129+
Safari17.5+17.5+Not yet
Edge117+117+117+
Opera103+103+103+

Global coverage: approximately 85–90% of modern browsers as of early 2026. Safari does not yet support overlay allow-discrete — popover/dialog closing animations fall back to instant exit in Safari.

Detecting Support

/* Detect transition-behavior: allow-discrete support */
@supports (transition-behavior: allow-discrete) {
  /* @starting-style also supported in these browsers */
  .element {
    transition: opacity 0.3s ease;
  }

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

/* Detect @starting-style specifically */
@supports at-rule(@starting-style) {
  @starting-style {
    .element { opacity: 0; }
  }
}

Frequently Asked Questions

Does @starting-style fire on every render or just the first?

@starting-style fires only when the element enters a new state for the first time: when it is first rendered (inserted into the DOM), or when it transitions from display: none to display: block. It does not re-fire if the element is already visible and a different property changes.

Can @starting-style be used without a CSS transition?

No. @starting-style only works when a CSS transition is applied to the element. Without a transition, the browser applies the starting styles and immediately jumps to the computed styles with no animation.

What is the difference between @starting-style and animation-fill-mode: backwards?

animation-fill-mode: backwards applies the from-keyframe values before an animation begins, preventing a flash of the final styles. @starting-style works with CSS transitions (not animations) and specifically targets the “element first appears” case where there is no previous computed style.

Does @starting-style work with CSS custom properties?

Yes. You can animate CSS custom properties (variables) using @starting-style as long as @property is used to register the property as animatable (CSS custom properties are not animatable by default).

Can I nest @starting-style inside another at-rule?

Yes. @starting-style can be nested inside @layer, @supports, @media, and other at-rules. It can also appear inside a regular style rule using CSS nesting.

Does @starting-style work with JavaScript-inserted elements?

Yes. @starting-style fires for elements inserted into the DOM via element.appendChild() or innerHTML, as long as the element has a CSS transition and matching @starting-style rules.

Related Tools

More CSS Tools