Home / Articles / Animation & Motion /

New Featureanimation

transition-behavior: allowing discrete transitions

Discrete properties like display, visibility, and overlay can now be animated with transition-behavior: allow-discrete.

The discrete property problem

For decades, CSS couldn't animate properties that flip between discrete values. display: block to display: none happens instantly — there's no intermediate state for the browser to interpolate. This forced developers into awkward workarounds: hiding with opacity: 0 while keeping elements in the layout, or reaching for JavaScript to stagger class changes.

The JavaScript approach typically looked like this: on hide, add a class that fades to opacity 0, then listen for the transitionend event to finally apply display: none. On show, remove display: none, wait a frame, then remove the opacity class. Two event listeners and a requestAnimationFrame just to fade something out. The transition-behavior property makes all of that unnecessary.

The transition-behavior syntax

Add transition-behavior: allow-discrete to transition a discrete property. It's always paired with a duration in the transition shorthand:

.modal {
  opacity: 0;
  display: none;
  transition: opacity 0.3s ease, display 0.3s allow-discrete;
}

.modal.is-open {
  opacity: 1;
  display: block;
}

The browser interpolates opacity over 300ms, holds display: block until the transition completes, then flips to display: none. Without allow-discrete, the property changes instantly and the animation is cut short.

You can also use the longhand transition-behavior property separately, but the shorthand form is more concise. The key constraint: allow-discrete only works for properties that have discrete interpolation — it doesn't make width or color behave differently.

Pairing with @starting-style

transition-behavior handles exits, but entering from display: none needs @starting-style to define the initial state. Together they create full entry-and-exit animations:

.toast {
  opacity: 1;
  transform: translateY(0);
  display: block;
  transition:
    opacity 0.25s ease,
    transform 0.25s ease,
    display 0.25s allow-discrete;

  @starting-style {
    opacity: 0;
    transform: translateY(1rem);
  }
}

.toast.is-hidden {
  opacity: 0;
  transform: translateY(-1rem);
  display: none;
}
Use @starting-style for entry animations and transition-behavior: allow-discrete for exit animations. They're complementary tools for the same problem.

Discrete properties you can now transition

Several previously untouchable properties are now animatable with allow-discrete:

  • display — add/remove elements from the layout
  • visibility — hide but keep in tab flow
  • overlay — show/hide top-layer elements like <dialog>
  • content-visibility — lazy-render off-screen regions

Each works the same way: add transition-behavior: allow-discrete to the transition list, and the browser holds the value until the transition completes. For visibility, the semantics differ slightly: transitioning to visibility: hidden holds it visible during the transition, while transitioning from hidden to visible makes the element immediately visible so the entering animation is seen.

Practical pattern: popover animations

Combine the Popover API with discrete transitions for CSS-only overlay animations. The overlay property needs special handling to keep the element in the top layer during exit:

[popover] {
  opacity: 1;
  scale: 1;
  transition:
    opacity 0.2s ease,
    scale 0.2s ease,
    display 0.2s allow-discrete,
    overlay 0.2s allow-discrete;

  @starting-style {
    opacity: 0;
    scale: 0.95;
  }
}

[popover]:not(:popover-open) {
  opacity: 0;
  scale: 0.95;
}

Without overlay in the transition list, the element drops out of the top layer instantly when hidden, making the exit animation invisible. This same pattern works for <dialog>: animate the ::backdrop pseudo-element alongside the dialog itself by adding overlay to the transition list of the dialog.

Browser support and strategy

transition-behavior and allow-discrete are available in Chrome 121+, Safari 17.5+, and Firefox 129+. Browser support is solid enough for production use in 2026. In older browsers, the element still shows and hides — the animation simply doesn't run on discrete properties. The fallback is graceful degradation, not broken behavior.

One pattern to avoid: don't use allow-discrete as a substitute for @starting-style on entry animations. allow-discrete ensures the discrete property doesn't snap during a transition; @starting-style defines what the initial state is. You need both for a complete enter/exit solution.

Use transition-behavior: allow-discrete for modern overlays, dialogs, and tooltips. The fallback (no animation on older browsers) is acceptable for most UX use cases.