Articles

CSS @starting-style Explained: Entry Transitions in CSS

CSS @starting-style defines an element's before-state on first render, so transitions can animate elements as they appear, even from display: none.

Takina Takina · · 4 min read
Close-up of CSS code on a screen

@starting-style is a CSS at-rule that defines the styles an element should transition from the first time it gets a style — when it’s added to the page or when it switches from display: none to visible. Ordinary CSS transitions can’t animate an element’s appearance because there’s no “previous” style to start from. @starting-style supplies that missing starting point, which makes fade-in and slide-in entry effects possible with pure CSS, no JavaScript class toggling required.

The problem it solves

CSS transitions run when a property changes from one computed value to another. That works fine for hover states, where an element already exists with a known style. It breaks down in two common situations:

  1. An element is inserted into the DOM. It’s rendered with its final styles on its very first frame. There’s no prior value, so nothing transitions.
  2. An element goes from display: none to displayed. Elements with display: none don’t have computed styles for rendering, so again the browser treats the first visible frame as the starting point.

The classic workaround was JavaScript: insert the element with an “initial” class, force a reflow or wait a frame with requestAnimationFrame, then swap to a “visible” class so the transition fires. It works, but it’s fiddly and easy to get wrong. CSS @keyframes animations can handle entry, but they don’t reverse smoothly when interrupted the way transitions do.

Basic syntax

You write @starting-style with the same selectors you’d use normally, describing the “before” state:

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

@starting-style {
  .toast {
    opacity: 0;
    transform: translateY(1rem);
  }
}

When a .toast element is added to the page, the browser first applies the starting styles, then transitions to the regular ones. The element fades and slides up into place.

You can also nest it inside the rule it applies to, using CSS nesting:

.toast {
  opacity: 1;
  transition: opacity 0.3s;

  @starting-style {
    opacity: 0;
  }
}

Starting styles only apply on an element’s first style update. They don’t interfere with later transitions, such as hover effects.

Animating from display: none

Entry is half the story. To animate an element that toggles between display: none and visible — popovers, dialogs, dropdowns — you need two more pieces.

First, display itself must be allowed to participate in the transition. It’s a discrete property: it can’t interpolate, it can only flip. The transition-behavior: allow-discrete declaration (or the allow-discrete keyword in the transition shorthand) tells the browser to flip discrete properties at the right moment. When showing an element, display flips at the start so the element is visible throughout the fade-in. When hiding, it flips at the end so the fade-out can finish before the element disappears.

Second, @starting-style provides the entry state:

.panel {
  display: none;
  opacity: 0;
  transition:
    opacity 0.25s,
    display 0.25s allow-discrete;
}

.panel.open {
  display: block;
  opacity: 1;
}

@starting-style {
  .panel.open {
    opacity: 0;
  }
}

Adding the open class now fades the panel in, and removing it fades the panel out before display: none takes effect.

Popovers and dialogs

This pattern is especially useful with the Popover API and the <dialog> element, both of which toggle between hidden and shown states and render in the top layer. The top layer adds one more wrinkle: when the element closes, it’s removed from the top layer immediately, which can cut the exit animation short. Transitioning the overlay property with allow-discrete keeps it in the top layer until the transition finishes:

[popover] {
  opacity: 0;
  transform: scale(0.95);
  transition:
    opacity 0.2s,
    transform 0.2s,
    overlay 0.2s allow-discrete,
    display 0.2s allow-discrete;
}

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

@starting-style {
  [popover]:popover-open {
    opacity: 0;
    transform: scale(0.95);
  }
}

The same approach works for a dialog’s ::backdrop, which you can fade in by giving it its own starting style.

Entry vs exit states

A common source of confusion: @starting-style only describes the entry state. The exit state is whatever the element’s styles are once it’s no longer open — in the popover example, the base [popover] rule. That means entry and exit can be different. You might slide a menu in from the left and fade it out in place, simply by giving the base rule and the starting style different values.

Things to watch out for

  • Specificity and order still apply. Starting styles cascade like any other rules. If your @starting-style block has lower specificity than the rule setting the final value, the starting value may never apply. Matching the selector exactly, or nesting the block, avoids surprises.
  • Not every property should animate. Stick to opacity and transform where you can; they’re the cheapest to animate. Transitioning layout properties like height or top forces layout work on every frame.
  • Respect reduced motion. Wrap motion-heavy entrances in a prefers-reduced-motion query, or fall back to a simple fade.
  • It’s progressive enhancement. Browsers that don’t support @starting-style simply ignore it, and elements appear instantly — a safe fallback. You can detect support with @supports if you need to branch.

When to use something else

@starting-style is ideal for entrance and exit effects on individual elements. For animating between two whole page or component states — such as a list reordering or a page navigation — the View Transitions API is a better fit. For looping or multi-step motion, @keyframes remains the right tool.

The takeaway

@starting-style gives an element a “before” style for its very first render, so normal CSS transitions can animate it onto the page. Pair it with transition-behavior: allow-discrete on display (and overlay for top-layer elements) to animate both entry and exit of popovers, dialogs, and toggled panels in pure CSS. Keep the animated properties cheap, respect reduced-motion preferences, and treat it as progressive enhancement.

Takina Takina · · 4 min read

Container Query Units: cqw, cqh, and cqi Explained

Container query units like cqw and cqi size elements relative to their containing element, not the viewport — here's how and when to use them.

#CSS #Web Development #Frontend