Debugging @starting-style That Does Not Apply

Part of CSS @starting-style & Entry Effects in Modern View Transitions & Scroll APIs.

The problem

A dialog is supposed to fade and scale in with @starting-style. It appears instantly. The rule looks correct in the stylesheet, the browser supports the feature, and a colleague’s copy of the same snippet works in a demo page. There is no error, no warning and nothing in DevTools that says “starting style ignored”.

@starting-style fails silently in a handful of specific ways. Each one leaves a recognisable trace once you know where to look.

Root cause analysis: six silent failures

1. No transition on the resting rule. @starting-style does not animate anything by itself; it only provides a before-change style. If the element’s normal rule does not declare transition for the properties that differ, the element simply renders in its final state. The most common mistake is putting transition inside the @starting-style block, where it applies to the fictional before state and has no effect on the real transition.

2. The element was not newly rendered. Starting styles apply on an element’s first style update — insertion into the document, or going from display: none to rendered. Toggling a class on an element that is already visible, or changing visibility or opacity, does not qualify. For those, an ordinary transition between two states is needed.

3. The starting style loses the cascade. Declarations inside @starting-style participate in the cascade like any other rule. If the starting-style selector is less specific than the resting rule, or appears earlier with equal specificity, the resting values win and the “before” state equals the “after” state. Nested @starting-style inside a rule inherits that rule’s selector, which avoids this; a separate top-level block needs a matching or stronger selector.

4. display changes without allow-discrete. When an element goes from display: none to visible, its starting style applies. But if display is not in the transition list with transition-behavior: allow-discrete, the reverse change removes the box on the first frame, so exits never show — and in top-layer elements, the missing overlay entry drops the element out of the top layer immediately. The entrance appears “sometimes” broken because the exit half never ran. Include display (and overlay for popovers and dialogs) with allow-discrete, as described in discrete and intrinsic-size animation.

5. The value is not transitionable. Starting a background-image gradient or an unregistered custom property from a starting style produces a discrete swap, not a fade.

6. Reduced motion is doing its job. A global transition: none under prefers-reduced-motion: reduce disables the entry. If DevTools is emulating the preference, the effect is correctly absent.

Symptom, trace and causeMost failures show as no transition in the Animations drawer.Symptom, trace and causeAnimations drawerComputed transitionLikely causeAppears instantly oninsertNothing recordednone or missing propertyNo transition on resting ruleAppears instantly onclass toggleNothing recordedPresentNot newly renderedInstant, transitionpresentNothing recordedPresentStarting style lost thecascadeSnaps halfwayRecorded, discretePresentProperty not interpolableInstant only withemulation onNothing recordednoneReduced motion working
Most failures show as no transition in the Animations drawer.

Step-by-step resolution

Diagnosing a missing entry effectWork from the transition outwards.Diagnosing a missing entry effect1Select the element and check the Computed transition-property and duration.Rules out cause 1 and 62Open the Animations drawer and trigger the entry.Did any transition start?3Check how the element appears: inserted, display change or class toggle.Rules out cause 24Move @starting-style inside the resting rule with CSS nesting.Same selector, no cascade loss5Add display with allow-discrete to the transition list if display changes.Rules out cause 46Turn off reduced-motion emulation and retest.
Work from the transition outwards.

Production code pattern

/* Broken: transition inside the starting style, separate weaker selector. */
@starting-style {
  dialog { opacity: 0; scale: 0.96; transition: opacity 200ms; }  /* transition does nothing here */
}
.modal[open] { opacity: 1; scale: 1; }                             /* no transition declared */

/* Fixed: transition on the resting rule, starting style nested in the same selector. */
.modal {
  opacity: 0;
  scale: 0.96;
  transition:
    opacity var(--motion-exit-duration) var(--motion-exit-easing),
    scale var(--motion-exit-duration) var(--motion-exit-easing),
    overlay var(--motion-exit-duration) allow-discrete,
    display var(--motion-exit-duration) allow-discrete;
}
.modal[open] {
  opacity: 1;
  scale: 1;
  transition:
    opacity var(--motion-enter-duration) var(--motion-enter-easing),
    scale var(--motion-enter-duration) var(--motion-enter-easing),
    overlay var(--motion-enter-duration) allow-discrete,
    display var(--motion-enter-duration) allow-discrete;

  @starting-style {
    opacity: 0;                     /* nested: inherits .modal[open] specificity */
    scale: 0.96;
  }
}

@media (prefers-reduced-motion: reduce) {
  .modal, .modal[open] { transition: opacity 100ms linear, overlay 100ms allow-discrete, display 100ms allow-discrete; }
  .modal { scale: 1; }
}
// Quick console check: does opening create any transitions?
dialog.showModal();
requestAnimationFrame(() => {
  console.table(dialog.getAnimations().map((a) => ({ type: a.constructor.name, prop: a.transitionProperty })));
});

Rendering Impact: composite for opacity and scale; overlay and display flip once. The fix does not change the cost of the effect — it makes the effect exist.

Nesting @starting-style inside the open rule is the most robust habit. The nested block takes the parent selector, so it can never be out-specified by the rule it modifies, and the starting and resting states sit next to each other where reviewers can compare them.

Where the transition declaration livesThe starting style supplies values; the resting rule supplies the transition.Where the transition declaration livestransition inside @starting-styleApplies to the imaginary before stateResting rule has no transitionElement renders at final valuesNo entry effecttransition on the resting ruleStarting style supplies from-valuesResting rule transitions to its valuesNested block matches specificityEntry effect runs
The starting style supplies values; the resting rule supplies the transition.

A minimal reproduction

When a large stylesheet hides the cause, strip the problem to a test page with one element, one resting rule with a transition, and one nested starting style. If the minimal page works, add the real stylesheet back in halves until the effect disappears; the half that breaks it contains a rule that either overrides the transition, out-specifies the starting style, or sets transition: none globally. This bisection is usually faster than reading computed styles for a component with many layered rules, and it works equally well for the popover and dialog patterns where top-layer behaviour adds variables.

Verification checklist

Constraints and trade-offs

  • @starting-style only applies on first render or leaving display: none, never on ordinary class toggles.
  • CSS nesting support is required for the nested form; top-level blocks need careful specificity.
  • Global transition resets in utility frameworks can silently remove the resting rule’s transition.
  • Starting styles for inherited properties can affect descendants’ first render too.
  • DevTools support for displaying starting-style rules is still evolving across browsers.

Frequently asked questions

Why does my @starting-style animation not run?

Most often the element’s normal rule has no transition for the properties, or the starting style rule loses the cascade. Put transition on the resting rule and nest @starting-style inside it.

Should transition go inside @starting-style?

No. The starting style describes the before state. The transition must be declared on the element’s normal rule.

Does @starting-style work when toggling a class?

Only if the toggle makes the element newly rendered, such as leaving display: none. For already-visible elements, use an ordinary transition between two states.

How can I tell if any transition started?

Open the Animations drawer or log element.getAnimations() on the next frame. No recorded transition means the effect was never created.