Home / Articles / Animation & Motion /
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;
}
@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 layoutvisibility— hide but keep in tab flowoverlay— 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.
transition-behavior: allow-discrete for modern overlays, dialogs, and tooltips. The fallback (no animation on older browsers) is acceptable for most UX use cases.