How @starting-style Can Fail You
The @starting-style at-rule is a newer CSS feature that promises to make entry animations easier. Instead of reaching for keyframe animations when you want an element to fade or slide into view, you provide an alternative set of declarations inside a @starting-style block. When the element is first created, the browser uses those initial styles and transitions from them to your main styles.
That works well in a basic case. An element with opacity: 0 inside @starting-style and opacity: 1 in its regular rule will fade in cleanly.
But there is a significant catch that can trip up real-world implementations: the styles inside @starting-style are not treated like keyframe styles. Keyframe declarations are promoted to their own high-priority collection, on par with user-agent styles and below !important. So a @keyframes rule will typically override any other specificity on the page.
@starting-style does not get that same treatment. It relies on standard CSS specificity rules. That works in simple setups where the initial declaration has equal or higher specificity than your other rules. But any rule that is more specific—an ID selector or an inline style—will can override the starting style before the transition ever fires.
A representative example
Consider a heading styled with an ID selector:
h1 { opacity: 1; }
If you try to animate that heading in with:
@starting-style {
h1 { opacity: 0; }
}
the browser computes the specificity of both rules. Since #title is more specific than the tag selector inside the starting-style block, the element initializes at opacity: 1. No transition occurs because the applied value never changes.
Modern CSS methodologies like Tailwind, CSS Modules, or BEM help isolate this problem somewhat, but they don't eliminate it. A realistic scenario where this gotcha appears is when you set end-state styles inline via JavaScript. Inline styles outrank any class or tag selector in the cascade.
For instance, if you generate particles and set their final transform from JavaScript while trying to start them centered via @starting-style, the inline transform wins. The particles will appear at their destination without animating.
Workarounds
When you hit this wall, a few options are available.
- Use
!important: You can promote the starting style to the highest priority group with:
@starting-style {
.particle {
transform: translate(0, 0) !important;
}
}
This solves the immediate problem, but it does restrict how you can structure your CSS in the future.
- Switch to CSS custom properties: If your real conflict is between an inline style in JavaScript and a class-based starting value, you can change what you set inline. Instead of giving the final
transformdirectly, set custom properties such as--xand--y. Then thetransformrule in your CSS file pulls from those variables:
@starting-style {
.particle {
transform: translate(0, 0);
}
}
.particle {
transform: translate(var(--x), var(--y));
}
Both declarations now live in the same specificity bucket, so the starting transition works as intended.
- Fall back to keyframes: The simplest path is often to skip
@starting-styleand use a keyframe animation instead. A short@keyframesrule for the entry effect will work in virtually every browser. It also avoids the specificity trap entirely because animation styles always get their own high-priority treatment. Many entry animations can be expressed with minimal keyframe code.
The choice depends on your constraints. !important is a quick patch; custom properties are elegant but add indirection; keyframes are more broadly compatible and conceptually familiar to most developers, even if they aren't thought of as a tool for entry animations.



