Part 3 of 4 · CSS Shine Lab

The @starting-style rule

The values an element animates from the first time it renders. The missing half of transition that lets things animate as they appear, even from display: none.

  • Innovation

Reach for @starting-style when you want an element to animate as it appears, whether it is on the page from the start or something you have just added, like an item a user creates and you drop into a list. Easing it in draws the eye to what is new. This one is more recent than transitions and keyframes, but it is supported in every major browser now. Firefox was last, in version 129, in August 2024.

Implementation

On its own, transition can only animate between two rendered states, so it cannot animate an element as it first appears: there is nothing to interpolate from. @starting-style is that missing before. Put your resting styles on the element as usual, add a transition, then list the initial values in a @starting-style block. For elements that appear out of display: none, such as popovers, tooltips, and dialogs, also transition display and overlay with transition-behavior: allow-discrete so they switch at the right end of the animation.

Fade and slide in on first render

The simplest form. The card eases from the values in its @starting-style block to its resting styles the first time it renders. Reload the page, or use Replay, to see it again.

I faded and slid in when the page loaded
View source

HTML

<div class="starting-card">I faded and slid in when the page loaded</div>

CSS

.starting-card {
  padding: 1rem 1.25rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface-elevated);
  color: var(--color-text);

  /* The resting ("after") state the element settles into. */
  opacity: 1;
  translate: 0 0;
  transition:
    opacity 0.6s ease,
    translate 0.6s ease;

  /* The "before" state, applied only for the element's very first render. The
     transition above interpolates from here to the resting state on load. */
  @starting-style {
    opacity: 0;
    translate: 0 1.5rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .starting-card {
    transition: none;
  }
}

A popover that animates open and closed

@starting-style earns its place with things that pop into existence. This native popover fades and scales in when opened, and allow-discrete on display and overlay lets it animate back out before it is hidden, with no JavaScript.

I animate in and out with @starting-style + allow-discrete
View source

HTML

<button popovertarget="starting-style-popover" class="starting-trigger">
  Toggle popover
</button>
<div id="starting-style-popover" popover class="starting-popover">
  I animate in and out with @starting-style + allow-discrete
</div>

CSS

.starting-trigger {
  padding: 0.7rem 1.2rem;
  border: none;
  border-radius: var(--radius-md);
  background: var(--color-accent);
  color: var(--color-on-accent);
  font: inherit;
  cursor: pointer;
}

/* The base rule doubles as the closed/exit state (transparent, scaled down);
   :popover-open below is the shown state. `allow-discrete` lets the normally
   un-animatable `display` and `overlay` take part, so the box can animate on
   the way OUT before `display` finally flips to none. */
.starting-popover {
  margin: auto;
  padding: 1rem 1.25rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface-elevated);
  color: var(--color-text);
  opacity: 0;
  scale: 0.95;
  transition:
    opacity 0.3s ease,
    scale 0.3s ease,
    display 0.3s ease allow-discrete,
    overlay 0.3s ease allow-discrete;
}

.starting-popover:popover-open {
  opacity: 1;
  scale: 1;
}

/* The starting state for the OPEN popover — the "before" for the entry
   animation. Needed because the popover appears from `display: none`, so there
   is no prior rendered state for `transition` to animate from. */
@starting-style {
  .starting-popover:popover-open {
    opacity: 0;
    scale: 0.95;
  }
}

@media (prefers-reduced-motion: reduce) {
  .starting-popover {
    transition: none;
  }
}

A tooltip that fades in on hover

The same technique gives a CSS-only tooltip. It fades in every time it appears on hover or focus, then fades out, with allow-discrete handling the switch in and out of display: none.

Hover or focus me A tooltip via @starting-style
View source

HTML

<span class="tooltip-host" tabindex="0">
  Hover or focus me
  <span class="tooltip-host__bubble" role="tooltip">A tooltip via @starting-style</span>
</span>

CSS

.tooltip-host {
  position: relative;
  border-bottom: 1px dashed var(--color-border-strong);
  cursor: help;
}

.tooltip-host__bubble {
  position: absolute;
  bottom: calc(100% + 0.5rem);
  left: 50%;
  translate: -50% 0;
  padding: 0.4rem 0.6rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: var(--color-surface-elevated);
  color: var(--color-text);
  white-space: nowrap;

  /* Hidden and transparent at rest. `allow-discrete` lets `display` flip at the
     right edge of the animation, so the bubble can fade out before it hides. */
  display: none;
  opacity: 0;
  transition:
    opacity 0.2s ease,
    display 0.2s ease allow-discrete;
}

.tooltip-host:hover .tooltip-host__bubble,
.tooltip-host:focus-visible .tooltip-host__bubble {
  display: block;
  opacity: 1;

  /* Fade in from transparent each time the tooltip appears. */
  @starting-style {
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .tooltip-host__bubble {
    transition: none;
  }
}

Back to CSS Shine Lab