Smooth appearance of a block in CSS: how to animate display: none
A block with display: none can be shown and hidden smoothly with CSS. We go through @starting-style and allow-discrete on working examples and look at browser support.
A block hidden with display: none can fade in and out smoothly in CSS with two techniques. The @starting-style rule sets the style the transition starts from, and display with the allow-discrete keyword in the transition list keeps the block on screen while it is being hidden. The entry animation works in all current browsers; a smooth exit so far works only in Chrome, Edge and Safari.
In short
- A regular
transitiondoes not run when a block leavesdisplay: none: the browser has no style to calculate the transition from. @starting-styleprovides that starting style. The rule works in Chrome and Edge 117, Firefox 129 and Safari 17.5.- The line
display 0.3s allow-discretein the transition list keeps the block on screen while it is being hidden. This works in Chrome and Edge 117 and in Safari 18; in Firefox the block disappears at once. - For
popoveranddialog,overlayis added to the transition list as well. Only Chrome and Edge understand this property. interpolate-size: allow-keywordsanimatesautoheight, also only in Chrome and Edge.- Nothing breaks in a browser without support: the block is shown and hidden without animation.
Why transition does not work with display: none
A CSS transition is calculated between two styles: the one before the change and the new one. While a block has display: none, the browser does not render it, so the block has no "before" style. According to the CSS Transitions specification, no transition starts for such an element, and the block appears in its final state right away.
In the example the card has an opacity transition, and the is-open class shows it.
.card {
display: none;
opacity: 0;
transition: opacity 0.3s ease;
}
.card.is-open {
display: block;
opacity: 1;
}
This code was tested in Chrome 154: 100 milliseconds after the class is added, opacity is already 1. Hiding fails for a different reason. The display property is discrete: there are no intermediate values between block and none. The none value applies at that same moment, and there is nothing left to animate opacity on.
How to show a block smoothly with @starting-style
The @starting-style rule sets the values a transition starts from when the block was not rendered before. We put it inside the open-state rule and list the starting values there.
.card {
display: none;
opacity: 0;
translate: 0 12px;
transition:
opacity 0.3s ease,
translate 0.3s ease;
}
.card.is-open {
display: block;
opacity: 1;
translate: 0 0;
@starting-style {
opacity: 0;
translate: 0 12px;
}
}
When the is-open class is added, the browser takes the values from @starting-style as the starting point and runs the transition to opacity: 1, so the card fades in and moves into place in 0.3 seconds.
The @starting-style rule gets no extra priority in the cascade. If it is written as a separate block above the main rule, the main rule overrides the starting values. With the nested form, as in the example, the order does not need attention.
The starting style applies every time the block appears, including the first render of the page. A card that already has the is-open class in the markup will also fade in when the page loads.
How to hide a block smoothly with allow-discrete
To make hiding smooth, the display property itself is added to the transition list with the allow-discrete keyword. The block then stays visible for the whole duration of the transition and gets display: none only at the end.
.card {
display: none;
opacity: 0;
translate: 0 12px;
transition:
opacity 0.3s ease,
translate 0.3s ease,
display 0.3s allow-discrete;
}
.card.is-open {
display: block;
opacity: 1;
translate: 0 0;
@starting-style {
opacity: 0;
translate: 0 12px;
}
}
The word allow-discrete enables a transition for a discrete property. Such properties normally flip at the midpoint of the transition, but the CSS Display specification makes an exception for display: when transitioning between none and any other value, the block counts as visible throughout.
@starting-style is not needed for hiding. Tab Atkins, one of the editors of the CSS specifications, explained this in a working group discussion:
"The reason for
@starting-styleto exist is becausedisplay:nonestomps over most styles and prevents them from having any meaningful value to animate from. But that's not the case for transitioning away, so there shouldn't be any need for an@ending-style."Tab Atkins, discussion in the CSSWG repository, March 14, 2026
What remains is toggling the class. A short JavaScript handler does that; the animation itself is described entirely in CSS.
<button class="toggle" type="button">Show or hide</button>
<div class="card">This block appears and disappears smoothly.</div>
document.querySelector('.toggle').addEventListener('click', () => {
document.querySelector('.card').classList.toggle('is-open');
});
The example was tested in Chrome 154. 100 milliseconds after the click, opacity was roughly halfway both on entry and on exit, and the block got display: none only after the transition ended.
All three snippets can be pasted into a new pen in Penlab: HTML, CSS and JS into their own panels. The preview updates by itself, and the duration or the offset can be changed right there.
How to animate popover and dialog
A popover element with the popover attribute and a modal dialog are animated the same way, but the overlay property is added to the transition list. The browser draws such elements in the top layer, above the rest of the page. On close it removes the element from the top layer at once, and the animation is cut off, while a transition on overlay postpones that until the animation ends.
A popover opens without JavaScript: the button is linked to it with the popovertarget attribute.
<button popovertarget="hint" type="button">Open hint</button>
<div id="hint" popover>The hint opens without JavaScript.</div>
[popover] {
opacity: 0;
scale: 0.95;
transition:
opacity 0.25s ease,
scale 0.25s ease,
display 0.25s allow-discrete,
overlay 0.25s allow-discrete;
}
[popover]:popover-open {
opacity: 1;
scale: 1;
@starting-style {
opacity: 0;
scale: 0.95;
}
}
Only the browser sets the value of overlay; in our styles we can only include it in transition. Right now Chrome and Edge understand the property. Other browsers skip an unknown name in the transition list, so the line does not get in their way.
A modal dialog is opened with the showModal() method. The dialog itself is animated like the popover, except that the open state is set by the open attribute.
<button class="open" type="button">Open dialog</button>
<dialog class="modal">
<p>The dialog and the backdrop appear smoothly.</p>
<form method="dialog">
<button>Close</button>
</form>
</dialog>
.modal {
opacity: 0;
translate: 0 16px;
transition:
opacity 0.3s ease,
translate 0.3s ease,
display 0.3s allow-discrete,
overlay 0.3s allow-discrete;
}
.modal[open] {
opacity: 1;
translate: 0 0;
@starting-style {
opacity: 0;
translate: 0 16px;
}
}
document.querySelector('.open').addEventListener('click', () => {
document.querySelector('.modal').showModal();
});
The dialog also has a dimmed backdrop, the ::backdrop pseudo-element. Its starting style is safer to write as a separate block: nesting for pseudo-elements started working in browsers later than @starting-style itself.
.modal::backdrop {
background-color: rgb(0 0 0 / 0);
transition:
background-color 0.3s ease,
display 0.3s allow-discrete,
overlay 0.3s allow-discrete;
}
.modal[open]::backdrop {
background-color: rgb(0 0 0 / 0.5);
}
@starting-style {
.modal[open]::backdrop {
background-color: rgb(0 0 0 / 0);
}
}
Both examples, the popover and the dialog with a backdrop, were tested in Chrome 154 and worked in both directions.
How to animate auto height
The interpolate-size: allow-keywords property enables the animation of expanding by height. Without it the browser cannot calculate a transition from a number to auto. The property is inherited, so it is set once on :root.
:root {
interpolate-size: allow-keywords;
}
.panel {
height: 0;
overflow: hidden;
visibility: hidden;
transition:
height 0.3s ease,
visibility 0.3s;
}
.panel.is-open {
height: auto;
visibility: visible;
}
We hide the panel with visibility: hidden because a block with zero height stays reachable from the keyboard and by a screen reader - a program that reads the page aloud. Browsers have animated visibility for a long time, and the panel stays visible during the transition.
Right now interpolate-size works only in Chrome and Edge from version 129, and in Firefox and Safari the panel opens and closes without a height animation.
Which browsers support this
As of October 11, 2026, the entry animation works in all current browsers, while the smooth exit and the other techniques are not supported everywhere. The data comes from the MDN compatibility tables and from the Firefox and Safari bug trackers, where browser developers keep track of bugs and tasks. Current versions on that date: Chrome and Edge 155, Firefox 157, Safari 27.
@starting-style: Chrome and Edge 117, Firefox 129, Safari 17.5. Since August 6, 2024 the rule has Baseline status: that is the label for features that work in recent versions of all major browsers.transition-behaviorand theallow-discretekeyword: Chrome and Edge 117, Firefox 129, Safari 17.4.- Transition of the
displayproperty: Chrome and Edge 117, Safari 18. Firefox does not support it; hiding there happens instantly. overlay: Chrome and Edge 117 only.interpolate-size: Chrome and Edge 129 only.- The
popoverattribute: Chrome and Edge 114, Firefox 125, Safari 17.
Work on the display transition in Firefox is in progress: in version 155 it appeared, but it is turned on by the layout.css.display-animations.enabled setting, which is off by default. Safari has no overlay, so on close a popover or a dialog loses its place in the top layer for the duration of the animation.
The examples in this article were tested only in Chrome 154. The behavior of Firefox and Safari is described from documentation and bug trackers, so it is worth checking in those browsers before using it in a project.
When opacity and visibility are enough
The older approach with opacity and visibility works in all browsers in both directions. It fits when the hidden block does not get in the way of the layout, for example a dropdown menu with absolute positioning: such a menu is already taken out of the page flow.
.menu {
opacity: 0;
visibility: hidden;
transition:
opacity 0.3s ease,
visibility 0.3s;
}
.menu.is-open {
opacity: 1;
visibility: visible;
}
The other common techniques have limitations.
opacity: 0alone. The invisible block keeps intercepting clicks. Withpointer-events: noneit can no longer be clicked with the mouse, but keyboard focus still lands on invisible links and buttons, and a screen reader reads the content.max-heightwith a margin. The transition runs to the value with the margin, so the visible duration does not match the specified one, and content taller than the margin gets cut off.- Moving the block off screen. The block stays reachable by focus and by a screen reader.
- JavaScript with
setTimeoutor thetransitionendevent. There is more code, and the event does not fire if the transition was interrupted.
A regular block hidden with visibility: hidden keeps taking up space in the flow. When that gets in the way of the layout, display: none and the approach from this article are needed.
How to find the mistake when the animation does not work
If the transition does not run, there are five places worth checking. Each mistake from the list below shows up in Stack Overflow questions or in browser bug trackers.
- The transition list has no
displaywithallow-discrete: the entry is smooth, the exit is instant. - The open-state rule has no
@starting-style: the exit is smooth, the entry is instant. @starting-styleis written as a separate block above the main rule, and the main rule overrides the starting values.- The
allow-discretekeyword is set with the separatetransition-behaviorproperty, and it comes before thetransitionshorthand. The shorthand resets it to the default value. - An exit animation is expected from
@starting-style. The rule only applies on entry.
According to the specification, a block that is being hidden is already inert: it is visible but does not react to clicks and focus. A button inside a disappearing card cannot be pressed, and that is the expected behavior.
What to try in the editor
The example is easier to understand by changing it by hand. The five steps below take about ten minutes.
- Open the Penlab editor and paste the HTML, CSS and JS of the card from the section about
allow-discrete. - Remove the line
display 0.3s allow-discreteand replace the comma on the previous line with a semicolon. Hiding becomes instant. - Put the line and the comma back, then delete the
@starting-styleblock. Now the entry happens abruptly. - Put the
@starting-styleblock back and replace the offset with scaling:translate: 0 12pxwithscale: 0.95in both places,translate: 0 0withscale: 1, and in the transition listtranslate 0.3s easewithscale 0.3s ease. - Increase the duration to one second in all three lines of the transition list, otherwise
displaywill flip before the animation ends.
A saved pen can be shared by link or embedded on your own page; we wrote about that in the post "Share a pen and embed it on your site". Other examples by members are collected in the showcase.
FAQ
Can a block be made to appear with no JavaScript at all?
Yes, if the browser itself toggles the state. A popover with the popover attribute is opened by a button with popovertarget, a details block expands on click, and the class can be replaced by the :has() selector with a checkbox. JavaScript is needed when the block is shown on an event that CSS does not have.
Is @starting-style needed for hiding?
No. The rule sets a style only for the entry. On exit the block transitions to the values of its base rule, and display with allow-discrete in the transition list is enough to make it smooth.
Can the entry of a block be animated with @keyframes?
Yes. display has been supported in keyframes since Chrome 116, and an animation needs neither @starting-style nor allow-discrete. A transition is more convenient because on a quick repeated click it reverses from the current point, while an animation starts over. Firefox does not animate display in keyframes.
What about users who have turned animation off?
For users with animation turned off, the transition is worth removing or shortening. The prefers-reduced-motion media query is supported by all current browsers.
@media (prefers-reduced-motion: reduce) {
.card {
transition: none;
}
}
What happens in a browser without support?
The block is shown and hidden without animation. Such a browser skips the @starting-style rule and drops the whole transition declaration with the unknown allow-discrete keyword, and the rest of the CSS works as usual.
Sources
- CSS Transitions Level 2, the section on @starting-style
- MDN: @starting-style
- Chrome for Developers: Four new CSS features for smooth entry and exit animations
- CSS Display Module Level 4, animating display
- MDN: transition-behavior
- WebKit: features in Safari 18.0
- CSS Transitions Level 1, starting of transitions
- MDN: overlay
- Chrome for Developers: Animate to height: auto
- Mozilla Bugzilla: display transition in Firefox
- MDN: interpolate-size
- web.dev: Baseline for entry animations
- WebKit Bugzilla: the overlay property
- MDN: visibility
- Vispero: how hiding methods affect accessibility
- MDN: Using the Popover API
- CSS-Tricks: Using CSS Transitions on Auto Dimensions