---
title: "Smooth appearance of a block in CSS: how to animate display: none"
description: "How to smoothly show and hide a block with display: none in CSS: @starting-style and allow-discrete, working examples and browser support."
url: https://penlab.space/blog/css-transition-display-none
language: en
section: "Articles"
published: 2026-10-12T15:56:47Z
updated: 2026-10-12T15:56:47Z
translation: https://penlab.space/blog/ru/css-transition-display-none
---

# 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 `transition` does not run when a block leaves `display: none`: the browser has no style to calculate the transition from.
- `@starting-style` provides that starting style. The rule works in Chrome and Edge 117, Firefox 129 and Safari 17.5.
- The line `display 0.3s allow-discrete` in 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 `popover` and `dialog`, `overlay` is added to the transition list as well. Only Chrome and Edge understand this property.
- `interpolate-size: allow-keywords` animates `auto` height, 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.

```css
.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.

```css
.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.

```css
.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-style` to exist is because `display:none` stomps 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](https://github.com/w3c/csswg-drafts/issues/9500#issuecomment-4059705893), March 14, 2026

What remains is toggling the class. A short JavaScript handler does that; the animation itself is described entirely in CSS.

```html
<button class="toggle" type="button">Show or hide</button>
<div class="card">This block appears and disappears smoothly.</div>
```

```js
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.

[Live example: Плавное появление и скрытие блока с display: none](https://penlab.space/pen/g6MBdP8e)

All three snippets can be pasted into a [new pen in Penlab](https://penlab.space/new): 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.

```html
<button popovertarget="hint" type="button">Open hint</button>
<div id="hint" popover>The hint opens without JavaScript.</div>
```

```css
[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;
  }
}
```

[Live example: Popover с анимацией открытия и закрытия](https://penlab.space/pen/Q9D8vjW8)

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.

```html
<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>
```

```css
.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;
  }
}
```

```js
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.

```css
.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);
  }
}
```

[Live example: Модальное окно dialog с анимацией и затемнением](https://penlab.space/pen/kLpOH58F)

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`.

```css
: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;
}
```

[Live example: Раскрытие блока по высоте auto](https://penlab.space/pen/tTnNcPsC)

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-behavior` and the `allow-discrete` keyword: Chrome and Edge 117, Firefox 129, Safari 17.4.
- Transition of the `display` property: 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 `popover` attribute: 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.

```css
.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: 0` alone. The invisible block keeps intercepting clicks. With `pointer-events: none` it 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-height` with 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 `setTimeout` or the `transitionend` event. 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.

1. The transition list has no `display` with `allow-discrete`: the entry is smooth, the exit is instant.
2. The open-state rule has no `@starting-style`: the exit is smooth, the entry is instant.
3. `@starting-style` is written as a separate block above the main rule, and the main rule overrides the starting values.
4. The `allow-discrete` keyword is set with the separate `transition-behavior` property, and it comes before the `transition` shorthand. The shorthand resets it to the default value.
5. 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.

1. Open the [Penlab editor](https://penlab.space/new) and paste the HTML, CSS and JS of the card from the section about `allow-discrete`.
2. Remove the line `display 0.3s allow-discrete` and replace the comma on the previous line with a semicolon. Hiding becomes instant.
3. Put the line and the comma back, then delete the `@starting-style` block. Now the entry happens abruptly.
4. Put the `@starting-style` block back and replace the offset with scaling: `translate: 0 12px` with `scale: 0.95` in both places, `translate: 0 0` with `scale: 1`, and in the transition list `translate 0.3s ease` with `scale 0.3s ease`.
5. Increase the duration to one second in all three lines of the transition list, otherwise `display` will 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"](https://penlab.space/blog/share-and-embed). Other examples by members are collected in the [showcase](https://penlab.space/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.

```css
@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](https://drafts.csswg.org/css-transitions-2/#defining-before-change-style)
- [MDN: @starting-style](https://developer.mozilla.org/en-US/docs/Web/CSS/@starting-style)
- [Chrome for Developers: Four new CSS features for smooth entry and exit animations](https://developer.chrome.com/blog/entry-exit-animations)
- [CSS Display Module Level 4, animating display](https://drafts.csswg.org/css-display-4/#display-animation)
- [MDN: transition-behavior](https://developer.mozilla.org/en-US/docs/Web/CSS/transition-behavior)
- [WebKit: features in Safari 18.0](https://webkit.org/blog/15865/webkit-features-in-safari-18-0/)
- [CSS Transitions Level 1, starting of transitions](https://drafts.csswg.org/css-transitions-1/#starting)
- [MDN: overlay](https://developer.mozilla.org/en-US/docs/Web/CSS/overlay)
- [Chrome for Developers: Animate to height: auto](https://developer.chrome.com/docs/css-ui/animate-to-height-auto)
- [Mozilla Bugzilla: display transition in Firefox](https://bugzilla.mozilla.org/show_bug.cgi?id=1834877)
- [MDN: interpolate-size](https://developer.mozilla.org/en-US/docs/Web/CSS/interpolate-size)
- [web.dev: Baseline for entry animations](https://web.dev/blog/baseline-entry-animations)
- [WebKit Bugzilla: the overlay property](https://bugs.webkit.org/show_bug.cgi?id=276727)
- [MDN: visibility](https://developer.mozilla.org/en-US/docs/Web/CSS/visibility)
- [Vispero: how hiding methods affect accessibility](https://vispero.com/resources/not-all-hidden-content-is-equal-how-different-hiding-methods-impact-accessibility/)
- [MDN: Using the Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API/Using)
- [CSS-Tricks: Using CSS Transitions on Auto Dimensions](https://css-tricks.com/using-css-transitions-auto-dimensions/)
