Modern CSS has revolutionized how we handle UI animations. For years, web developers struggled with a fundamental limitation: CSS transitions couldn't animate elements to or from display: none. The CSS Transitions Level 2 specification changed everything by introducing @starting-style and transition-behavior. These powerful features finally allow developers to create seamless entry and exit animations using pure CSS.
This guide explores how to animate dialogs, popovers, and modal interfaces without JavaScript animation libraries, leveraging browser-native capabilities for optimal performance. By mastering these techniques, you can create polished, professional interfaces that enhance user experience while keeping your codebase clean and maintainable as part of a comprehensive web development strategy.
These animation principles complement other modern CSS techniques like styling and animating SVGs with CSS for dynamic vector graphics and CSS gradient background animations for buttons for engaging UI interactions.
The Problem: Why Display None Was Untransitionable
The CSS Transitions Level 1 specification defined transitions as interpolating between two computed property values. For properties like opacity or transform, this works straightforwardly--the browser calculates intermediate values. However, for the display property, there's no meaningful intermediate value between none and block.
What Are Discrete Properties?
In CSS terminology, properties fall into three animation categories:
- Interpolable: Can smoothly transition (opacity, transform, color)
- Discrete: Switch at specific points (display, visibility, overlay)
- Not animatable: Cannot be transitioned at all
Discrete properties like display cannot be interpolated because their values are keywords rather than numbers. When changing from display: block to display: none, the browser immediately switches the value with no intermediate states.
Legacy Workarounds
Before CSS Transitions Level 2, developers relied on JavaScript workarounds to create entry and exit animations. Common approaches included:
-
setTimeout hacks: Setting opacity to 0, waiting for the next frame, then setting display: none--this created race conditions and timing issues.
-
CSS animation keyframes: Using
@keyframesinstead of transitions, which allowed for multi-step animations but didn't solve the fundamental problem. -
JavaScript animation libraries: Libraries like GSAP or anime.js provided their own mechanisms for handling these state changes, adding significant bundle size.
-
CSS-first approaches: Some developers would animate opacity to 0, listen for
transitionend, then set display: none--but this required complex JavaScript coordination.
According to the W3C CSS Transitions Level 2 Specification, the inability to transition discrete properties was a fundamental limitation that required browser-level changes to address properly.
Understanding @starting-style
The @starting-style at-rule represents a paradigm shift in how we think about CSS animations. Traditionally, transitions required an element to have a "before-change style" established before transitioning to a new state. For elements that don't yet exist or are transitioning from display: none, there was no before style to reference.
@starting-style solves this by explicitly defining what values an element should have at the very beginning of its render lifecycle. When an element is about to become visible, the browser looks inside @starting-style blocks to determine what values should apply at the transition's origin point.
As explained in Smashing Magazine's comprehensive guide, this gives developers precise control over entry animations, enabling effects like fading in from zero opacity, sliding in from off-screen positions, or scaling up from a reduced size.
Syntax and Usage
.dialog[open] {
opacity: 1;
transform: scale(1);
@starting-style {
opacity: 0;
transform: scale(0.95);
}
}
This tells the browser: "When the dialog opens, animate from these starting values to the final values." The @starting-style block provides the initial state, while the element's normal CSS defines the target state.
The LogRocket tutorial on dialog animations demonstrates how this pattern works seamlessly with the <dialog> element's native API.
The transition-behavior Property
While @starting-style handles entry animations, transition-behavior: allow-discrete enables exit animations for discrete properties. This property tells the browser to treat discrete properties (like display, visibility, and overlay) as transitionable, controlling exactly when these properties switch values.
Syntax and Values
.dialog {
transition: opacity 300ms, display 300ms allow-discrete, overlay 300ms allow-discrete;
}
The key values are:
- normal: Default behavior, discrete properties switch immediately
- allow-discrete: Delays discrete property changes until transition end
As documented by CSS-Tricks, the magic of allow-discrete lies in its timing control. Without it, discrete properties switch at the beginning of a transition (for display and content-visibility) or at the 50% mark (for most other discrete properties). With allow-discrete, developers can ensure discrete property changes happen at the transition's end.
When Discrete Properties Switch
Understanding when discrete properties switch is crucial for creating effective animations. For display and content-visibility, the switch happens at the beginning of the transition (0% mark) by default. With allow-discrete, the discrete property change is delayed until the transition completes, ensuring that opacity fades, transforms, and other interpolable properties finish their animations before the element is removed from the document flow.
Animating Dialog and Popover Elements
The <dialog> element and Popover API represent the modern approach to modal interfaces. These elements automatically enter the browser's top layer--a special rendering context above the document's main stacking context. Properly animating dialogs requires coordinating transitions for opacity, transform, display, and overlay properties simultaneously.
Complete Dialog Animation Pattern
/* Base dialog styles */
dialog {
opacity: 0;
transform: scale(0.95) translateY(20px);
transition:
opacity 300ms ease-out,
transform 300ms ease-out,
display 300ms allow-discrete,
overlay 300ms allow-discrete;
}
/* Open state with starting styles */
dialog[open] {
opacity: 1;
transform: scale(1) translateY(0);
@starting-style {
opacity: 0;
transform: scale(0.95) translateY(20px);
}
}
/* Backdrop animation */
dialog::backdrop {
background-color: rgba(0, 0, 0, 0);
transition: background-color 300ms ease-out, display 300ms allow-discrete;
}
dialog[open]::backdrop {
background-color: rgba(0, 0, 0, 0.5);
@starting-style {
background-color: rgba(0, 0, 0, 0);
}
}
JavaScript Integration
The JavaScript needed to open and close a dialog is minimal--these animations work automatically once the CSS is in place:
// Opening the dialog
const dialog = document.querySelector('dialog');
dialog.showModal(); // Triggers entry animation via @starting-style
// Closing the dialog
dialog.close(); // Triggers exit animation via transition-behavior
The key insight is that JavaScript only controls when the dialog opens and closes--the browser handles all animation timing internally. The dialog's open attribute presence triggers the entry animation, while its removal triggers the exit animation. This declarative approach eliminates the need for manual timing coordination that plagued earlier JavaScript-heavy animation solutions.
For popover elements using the Popover API, the pattern is identical--simply replace dialog with your popover element and use element.showPopover() and element.hidePopover() methods.
Performance Considerations
CSS-based animations generally perform better than JavaScript animations because browsers can optimize them using the compositor thread. However, certain animated properties can still cause performance issues. These animation techniques work seamlessly with other modern CSS features like infinite scrolling logos with HTML and CSS and CSS browser support considerations.
Performant Properties
| Property | Performance | Notes |
|---|---|---|
| transform | Excellent | Handled by compositor |
| opacity | Excellent | Handled by compositor |
| color | Good | Requires paint |
| background-color | Good | Requires paint |
| width/height | Poor | Triggers layout recalc |
| margin/padding | Poor | Triggers layout recalc |
For dialogs and popovers, stick to transform and opacity for the smoothest results. These properties can be animated at 60fps or higher on most devices without causing layout thrashing.
Accessibility: prefers-reduced-motion
Respect user preferences for motion using the prefers-reduced-motion media query:
@media (prefers-reduced-motion: reduce) {
dialog {
transition: none !important;
}
dialog[open] {
opacity: 1;
transform: none;
}
}
This ensures users who experience discomfort from motion animations receive a static, functional interface while still maintaining full dialog functionality.
Browser Support and Progressive Enhancement
| Browser | @starting-style | transition-behavior |
|---|---|---|
| Chrome | ✅ Full | ✅ Full |
| Edge | ✅ Full | ✅ Full |
| Safari | ✅ Full | ✅ Full |
| Firefox | ✅ Full | ✅ Limited |
Feature Detection
@supports (transition-behavior: allow-discrete) {
/* Enhanced animations for capable browsers */
dialog {
transition: opacity 300ms, display 300ms allow-discrete;
}
}
@supports not (transition-behavior: allow-discrete) {
/* Functional fallback - no animation */
dialog {
transition: opacity 300ms;
}
}
Progressive Enhancement
The most important principle when implementing these animations is functionality must work without animations. Every user should be able to open and close dialogs regardless of their browser's animation capabilities. Browsers that support @starting-style and transition-behavior receive enhanced animations; those that don't receive a functional but instantaneous state change.
This approach aligns with our web development philosophy at Digital Thrive--building interfaces that work reliably across all browsers and devices while progressively enhancing the experience for capable browsers. The core functionality (opening and closing dialogs) remains the priority, with animations serving as a polish layer for users who can appreciate them.