How to Transition Display with CSS allow-discrete

Historically, hiding an element by setting its CSS display to none caused an immediate disappearance, instantly cutting off any active exit transitions like fades or slides. The declaration transition: display 0.3s allow-discrete solves this issue by enabling transitions on discrete properties and delaying the switch to display: none until the specified transition duration has finished. This article explains how the allow-discrete keyword alters browser rendering rules, prevents premature removal from the document flow, and enables seamless exit animations.

The Problem with Discrete Properties

In CSS, properties fall into two categories: interpolable and discrete. Interpolable properties, such as opacity, height, and transform, have continuous numeric values that a browser can smoothly calculate frame-by-frame between states.

Discrete properties, such as display, visibility, or content, do not have intermediate values. An element cannot be "half block and half none." By default, the browser executes discrete property changes instantly at \(t = 0\) of an animation frame. When display: none is applied alongside an opacity fade-out:

  1. The browser immediately applies display: none.
  2. The element is instantly stripped from the layout and render tree.
  3. The accompanying opacity transition never renders because non-displayed elements are not painted.

How allow-discrete Changes the Rendering Engine

The allow-discrete keyword is a value of the transition-behavior property (included here via the transition shorthand). It explicitly instructs the browser's transition engine to allow discrete property types to participate in transitions.

When allow-discrete is applied, the browser modifies the timeline of how the property flips between its initial and final values:

  • When transitioning away from a visible display value to none (Exit): The browser holds the element at its visible state (e.g., block, flex, grid) for the entire duration (e.g., 0.3s). The property only swaps to display: none at the very end of the transition timeline (\(t = 1.0\)).
  • When transitioning from display: none to a visible value (Entry): The browser swaps the property immediately at the start (\(t = 0.0\)) to make the element renderable, allowing concurrent animations (often paired with @starting-style) to be visible immediately.

Preventing Abrupt Element Removal

By deferring the execution of display: none to the end of the specified time period, transition: display 0.3s allow-discrete creates a grace period. During this window, the element remains part of the render tree.

This enables other transitions running over the same duration to complete without being interrupted:

.card {
  opacity: 1;
  transform: scale(1);
  display: block;
  transition: 
    opacity 0.3s ease,
    transform 0.3s ease,
    display 0.3s allow-discrete;
}

.card.is-hidden {
  opacity: 0;
  transform: scale(0.95);
  display: none;
}

In this scenario, when .is-hidden is added:

  1. opacity begins interpolating from 1 to 0 over 300ms.
  2. transform begins scaling from 1 to 0.95 over 300ms.
  3. Because of allow-discrete, display remains block for the entire 300ms duration.
  4. Once the 300ms timer elapses and the visual fade completes, the browser switches the property to display: none, safely removing the element from layout calculation, screen readers, and tab order without any visible snap.