Complete CSS Course Module 16: Advanced: Cutting-Edge CSS

@property, @starting-style and Animating to display: none

  • Beginner
  • Lesson 16.6
  • 5 min read
  • 72 of 77 in the course

In short

CSS @property lets you animate custom properties like gradient angles, @starting-style creates entry animations, and allow-discrete lets you animate to display: none. Why can't you animate a gradient normally? Try this: a button with a gradient, and on hover a different gradient with a transition.

CSS @property lets you animate custom properties like gradient angles, @starting-style creates entry animations, and allow-discrete lets you animate to display: none.

Why can't you animate a gradient normally?

Try this: a button with a gradient, and on hover a different gradient with a transition. The gradient jumps instantly, without any smooth change. Browsers can't blend between two background-image values. Custom properties don't help by default either: to the browser, --angle: 90deg is just text, and it doesn't know how to go from one piece of text to another.

@property fixes this. It registers a custom property and tells the browser its type, like <angle>, <color>, <length> or <percentage>. Once the browser knows --angle is an angle, it can animate it from 0deg to 360deg smoothly. And because the gradient uses the variable, the gradient changes too.

Animating gradients with @property

<div class="glow-card">
  <h3>Pro plan</h3>
  <p>Everything you need to learn faster.</p>
</div>
@property --angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

.glow-card {
  padding: 24px;
  border: 4px solid transparent;
  border-radius: 16px;
  background:
    linear-gradient(white, white) padding-box,
    conic-gradient(from var(--angle), #2563eb, #db2777, #f59e0b, #2563eb) border-box;
  animation: spin 4s linear infinite;
}

@keyframes spin {
  to {
    --angle: 360deg;
  }
}

The card has a colorful border that slowly rotates around it, like a glowing light. The white padding-box background covers the inside, so only the border shows the gradient. The three descriptors are all required: syntax (the type), inherits (should children get the value?) and initial-value (the starting value).

The same works for transitions. Here a button changes its gradient color smoothly on hover:

@property --start {
  syntax: "<color>";
  inherits: false;
  initial-value: #2563eb;
}

.gradient-btn {
  background: linear-gradient(135deg, var(--start), #7c3aed);
  color: white;
  transition: --start 0.4s;
}

.gradient-btn:hover {
  --start: #db2777;
}

On hover, the left side of the gradient fades from blue to pink, instead of jumping.

Entry animations with @starting-style

A transition needs a "before" value and an "after" value. But when an element appears for the first time (added to the page, or changed from display: none), there is no "before", so nothing animates. @starting-style gives the browser that missing "before" state.

.toast {
  position: fixed;
  bottom: 20px;
  right: 20px;
  padding: 16px 20px;
  background: #111827;
  color: white;
  border-radius: 10px;
  opacity: 1;
  translate: 0 0;
  transition: opacity 0.3s, translate 0.3s;
}

@starting-style {
  .toast {
    opacity: 0;
    translate: 0 20px;
  }
}

When a .toast notification is added to the page (for example with JavaScript after saving a form), it starts invisible and 20px lower, and then slides up and fades in. No JavaScript timer or extra class is needed for the animation.

Animating to display: none with allow-discrete

Leaving is the other half of the problem. display is a discrete property: it has no values in between, it is either none or not. Normally it switches immediately, so the element disappears before any fade-out can be seen. transition-behavior: allow-discrete tells the browser to wait: keep the element visible during the transition and switch display at the very end.

Here is a popover menu that fades and scales both in and out. A popover is a built-in HTML feature for menus and tooltips: the button opens and closes it without JavaScript.

<button popovertarget="user-menu">My account</button>

<div id="user-menu" popover>
  <a href="/profile">Profile</a>
  <a href="/settings">Settings</a>
  <a href="/logout">Log out</a>
</div>
#user-menu {
  padding: 12px;
  border: none;
  border-radius: 12px;
  box-shadow: 0 10px 30px rgb(0 0 0 / 0.2);
  opacity: 0;
  scale: 0.95;
  transition:
    opacity 0.2s,
    scale 0.2s,
    display 0.2s,
    overlay 0.2s;
  transition-behavior: allow-discrete;
}

#user-menu:popover-open {
  opacity: 1;
  scale: 1;
}

@starting-style {
  #user-menu:popover-open {
    opacity: 0;
    scale: 0.95;
  }
}

Click "My account" and the menu grows and fades in. Click outside or press Escape and it shrinks and fades out, and only then becomes display: none. Three pieces work together: the closed style (opacity 0), the open style (:popover-open), and @starting-style for the very first frame. Adding display and overlay to the transition with allow-discrete keeps the menu visible and on top while it closes.

The same pattern works for a modal <dialog>, opened with dialog.showModal(): use dialog[open] for the open state and you can also fade its ::backdrop.

Note: @property, @starting-style and transition-behavior work in all current major browsers. The overlay property is Chromium-only for now; other browsers ignore it, and the close animation may simply be shorter there. That is a safe enhancement.

Common mistake: Putting @starting-style before the normal rule with the same selector. It has the same specificity as the normal rule, so the later one wins. Write @starting-style after the element's normal styles.

More details on MDN's @starting-style page.

Practice exercise

Build a "Cookie settings" <dialog> with a close button. Make it fade and slide down when it opens, fade out when it closes, and fade its ::backdrop from transparent to a dark tint. Then give the "Save" button inside it an animated gradient border using @property.

Hint: style dialog[open] and dialog[open]::backdrop, add both to @starting-style, and include display and overlay in the transition with allow-discrete.

Key takeaways

  • @property registers a custom property with a type so it can be animated.
  • Animate a registered --angle or --color to animate gradients.
  • @starting-style defines the "before" state for elements that just appeared.
  • transition-behavior: allow-discrete lets display switch at the end of a transition.
  • Popovers and dialogs can now animate both open and closed with CSS only.