EmpireUI
Get Pro
← Blog7 min read#css-animation#starting-style#enter-animation

@starting-style: Native CSS Enter Animations Are Finally Here

@starting-style lets you animate elements on entry with pure CSS — no JavaScript, no libraries. Here's what it is, how it works, and when to actually use it.

Lines of CSS code on a dark monitor screen representing native CSS animation techniques

What @starting-style Actually Does

Honestly, we've been duct-taping JavaScript onto CSS transitions for years just to animate something when it first appears on screen. We'd set a class, flip it on the next frame, cross our fingers. It worked, but it was always a hack.

@starting-style is a CSS at-rule that lets you define where a transition should start from — before the element's computed styles are applied. The browser reads it, stores those "starting" values, and then transitions to whatever the element's actual styles are. All in CSS. No requestAnimationFrame, no setTimeout(fn, 16).

It shipped in Chrome 117 and Safari 17.5. Firefox landed it in version 129. As of late 2026, baseline support is solid enough to use in production with a light fallback. That's the real news here — this isn't a preview flag experiment anymore.

The Syntax: Simpler Than You'd Expect

The rule lives inside a regular CSS transition block. You write @starting-style nested inside the element's ruleset, or as a top-level block targeting the same selector. Both forms work in current browsers, though nested is cleaner.

Here's a dropdown panel animating in from opacity 0 and a 6px upward offset:

.dropdown-panel {
  opacity: 1;
  transform: translateY(0);
  transition:
    opacity 200ms ease,
    transform 200ms ease;
}

@starting-style {
  .dropdown-panel {
    opacity: 0;
    transform: translateY(-6px);
  }
}

That's it. When .dropdown-panel is added to the DOM — or transitions from display: none to display: block — the browser starts the transition from opacity 0 and -6px, then eases to the declared values. No JS class toggling. No mutation observer. Just CSS doing what CSS should have done five years ago.

display: none and the Discrete Animation Problem

Here's the thing: transitions have always had a blind spot. You can't transition out of display: none because display is a discrete property — it doesn't interpolate. The element just pops in.

Starting with Chrome 116, the transition-behavior: allow-discrete property was added to handle this. Pair it with @starting-style and you get genuinely smooth enter animations on elements that were previously hidden with display: none. That's a real fix for dialog elements, popovers, and any show/hide toggle pattern.

.modal {
  display: block;
  opacity: 1;
  scale: 1;
  transition:
    opacity 250ms ease,
    scale 250ms ease,
    display 250ms allow-discrete;
}

@starting-style {
  .modal {
    opacity: 0;
    scale: 0.96;
  }
}

.modal:not([open]) {
  display: none;
  opacity: 0;
  scale: 0.96;
}

The allow-discrete keyword tells the browser to keep the element in the render tree long enough to finish the transition before actually hiding it. Exit animations fall out of this naturally — no JS needed for either direction.

How It Compares to JavaScript Animation Libraries

Before @starting-style, the standard move was Framer Motion's AnimatePresence, or a lightweight wrapper around the Web Animations API. Both are fine tools. But they add weight — Framer Motion at its smallest tree-shaken chunk is still north of 30 kB gzipped.

For simple opacity-and-translate entrance animations, that trade-off made less and less sense. @starting-style gets you the same visual result at zero bundle cost. The question worth asking is: what do you actually need that a library adds? If the answer is complex choreography, spring physics, or gesture tracking — use the library. If it's just "fade in and slide up when this thing appears" — @starting-style handles it.

This is the same argument that's been playing out between custom scroll animations and the native Intersection Observer API, or between Tippy.js and the native Popover API. Native catches up. It always does. The trick is recognizing when it's good enough for your actual use case, not the theoretical one.

If you're building a React component library with heavier animation needs, pairing @starting-style with something like a particles background or an aurora background still makes sense for ambient effects — those aren't enter animations, they're continuous renders.

Using @starting-style With Tailwind CSS

Tailwind v4.0.2 doesn't ship a @starting-style utility out of the box. You'll write it in your global CSS or a component stylesheet. That's not a knock — Tailwind is smart to not abstract this until the API stabilizes across browsers.

The cleanest pattern right now is a @layer utilities block where you define named entering states you can compose on any element:

@layer utilities {
  .entering {
    @starting-style {
      opacity: 0;
      transform: translateY(8px);
    }
    transition: opacity 200ms ease, transform 200ms ease;
  }

  .entering-scale {
    @starting-style {
      opacity: 0;
      scale: 0.95;
    }
    transition: opacity 180ms ease, scale 180ms ease;
  }
}

Apply .entering or .entering-scale directly in JSX with className. No JS. Works with React, Next.js, plain HTML — anything that renders real DOM. If you're comparing this approach to CSS Modules versus Tailwind for scoping, the answer here is the same: use whatever owns your styles already.

Popover API + @starting-style: A Natural Pairing

The native Popover API landed alongside @starting-style in the same browser release wave, and they fit together almost perfectly. Popovers use display: none to hide, which means they previously had no transition story. Now they do.

Any element with popover="auto" or popover="manual" can animate in using the :popover-open pseudo-class as the transition target and @starting-style for the from-state. Browser handles show/hide, CSS handles animation. No library required.

[popover] {
  opacity: 0;
  translate: 0 4px;
  transition: opacity 150ms ease, translate 150ms ease,
    display 150ms allow-discrete,
    overlay 150ms allow-discrete;
}

[popover]:popover-open {
  opacity: 1;
  translate: 0 0;
}

@starting-style {
  [popover]:popover-open {
    opacity: 0;
    translate: 0 4px;
  }
}

The overlay property in the transition list is what keeps the element in the top layer during exit — another discrete property that needs allow-discrete. It's a small gotcha that trips people up the first time.

Browser Support, Fallbacks, and When to Ship It

Chrome 117+, Edge 117+, Safari 17.5+, Firefox 129+. That's roughly 93% global coverage as of Q4 2026. For most apps, that's ship-it territory. Browsers that don't support @starting-style simply skip the at-rule — the element appears without animation, which is a perfectly acceptable fallback.

If you need the animation on older Safari (pre-17.5), the @supports route works: wrap your @starting-style block in @supports (selector([popover])) or @supports (transition-behavior: allow-discrete) and serve a JS-based alternative only where needed. That's rare and probably not worth the complexity for most projects.

When should you hold off? If your users are on locked-down enterprise browsers, or if the animation is load-bearing for UX comprehension rather than just polish. But for a tooltip fading in or a menu sliding down? Shipping @starting-style now is the right call. Check if any animated background effects you're already using are pulling in JS you could eliminate at the same time.

Real Patterns Worth Copying

A few enter animation patterns come up constantly in practice. Here they are without ceremony.

Toast notifications entering from the bottom-right: start at translateY(16px) opacity 0, transition over 240ms ease-out. Dialogs scaling in from 0.97: opacity 0 scale 0.97, 180ms ease. Navigation drawers sliding from -100%: translateX(-100%), 280ms cubic-bezier(0.4, 0, 0.2, 1). Cards fading in on grid load: stagger using animation-delay on nth-child, @starting-style handles the from-state for each.

The stagger case deserves a note. @starting-style fires when the element first gets a computed style, so elements added to the DOM at different times each get their own enter animation triggered independently. You don't need a JS stagger manager — just add elements sequentially and each one animates in on its own.

If your UI includes heavier ambient effects like a spotlight effect or a shooting stars background, @starting-style can handle the content layer's enter animations while those effects run underneath. They don't interfere with each other.

FAQ

Does @starting-style work with React and Next.js?

Yes. @starting-style is pure CSS and fires based on when the browser first computes an element's styles. It doesn't care whether the DOM was built by React, Next.js, or static HTML. The only requirement is that your CSS is loaded before the element renders — which is standard in any properly configured Next.js app.

Why do I need transition-behavior: allow-discrete?

Properties like display and visibility are discrete — they switch instantly rather than interpolating. Without allow-discrete, the browser won't hold an element in the render tree long enough to complete its exit transition, and it won't animate in from display: none either. Add it to your transition list for any property that's discrete.

Can I use @starting-style with CSS keyframe animations instead of transitions?

@starting-style is specifically for CSS transitions, not keyframe animations. For keyframes, you'd use animation-fill-mode: backwards with a from block instead — that's been supported for years. @starting-style is the native solution for transitions that need a defined starting point.

What happens in browsers that don't support @starting-style?

Browsers that don't recognize @starting-style skip the at-rule entirely and show no error. The element just appears without animation. That's a clean, acceptable fallback — the UI still works, it just doesn't animate. You don't need a polyfill unless the animation is genuinely required for usability.

Is @starting-style the same as the CSS @keyframes from block?

No. @keyframes from defines the start of a looping or one-shot animation that runs independently of element state. @starting-style defines where a CSS transition begins when an element first receives computed styles. They solve similar visual problems but through different mechanisms — transitions respond to property changes, keyframes run on a timeline.

Can I animate multiple properties with different timing using @starting-style?

Yes. Define each property's starting value in @starting-style and give each its own timing in the transition shorthand. For example: transition: opacity 200ms ease, transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1). Each property transitions independently from its starting value. You get full control over per-property duration and easing.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Animation Design in 2027: AI-Generated Motion, CSS NativeCSS Flip Card Animation: 3D Reveal Without JavaScriptDark Card Hover Animations: Transform, Glow, Scale EffectsNative <dialog> Element: Modals Without React State or Libraries