Animating display in Keyframe Animations

Part of Discrete & Intrinsic-Size Animation in Modern View Transitions & Scroll APIs.

The problem

An onboarding tip should appear two seconds after page load, stay for six seconds, fade out and then stop occupying layout. A splash overlay should fade out and then become display: none so it stops blocking clicks. Doing this with classes and timers means three timeouts and three class changes. Doing it with transitions and allow-discrete needs state changes to trigger each phase.

A keyframe animation can hold the whole sequence — including the display changes — in one declaration.

Root cause analysis: discrete values flip between keyframes

Discrete properties are allowed in keyframes. Keyframe animations have long been able to set properties that do not interpolate. For such a property, the value flips at the midpoint of the interval between two keyframes that set different values. display is now animatable this way too.

The midpoint rule usually needs help. An exit from display: block at 0% to display: none at 100% would flip at 50% — halfway through the fade. To keep the element rendered until the end, set display: block on a keyframe just before the end, such as 99.9%, so the interval containing the flip is vanishingly short. Alternatively, repeat display: block on every intermediate keyframe.

Entrances flip early the same way. For an element starting at display: none, put display: block on 0% of the entrance keyframes — or rely on the base style being block — so it is rendered from the first frame.

Fill decides what happens after. An exit animation that ends at display: none needs animation-fill-mode: forwards to stay hidden, or the base style must already be display: none with the animation making it visible for its duration. The second approach avoids a permanent fill, the trade-off described in animation-fill-mode explained.

Keyframes versus transitions. Transitions with allow-discrete flip display at the visibility-preserving end automatically and reverse from their current position, which suits toggles. Keyframes suit fixed, time-driven sequences that are not interrupted: timed tips, splash screens, one-shot reveals.

A timed tip in one keyframe animationdisplay is block from 25% to 99.9% of an 8 s animation; the element occupies no layout before or after.A timed tip in one keyframe animationdisplaynoneblock8 sopacity018 snoneblock0fade in1fade out
display is block from 25% to 99.9% of an 8 s animation; the element occupies no layout before or after.

Step-by-step resolution

Building a self-running show and hide sequencePut the flips where the fades need them.Building a self-running show and hide sequence1Make the base style display: none so the element starts and ends hidden.No fill needed afterwards2Add a keyframe at the start of the visible period with display: block and opacity: 0.Rendered from its first visible frame3Add the faded-in keyframe shortly after.Entrance fade4Keep display: block through the fade-out keyframe at 99.9%.Rendered until the fade ends5Let the 100% keyframe be display: none.Space released at the end6Under reduced motion, skip the fades but keep a readable visible period.
Put the flips where the fades need them.

Production code pattern

/* Timed onboarding tip: hidden, then visible for ~6s, then gone. */
.tip {
  display: none;                                  /* base style: start and end hidden */
  animation: tip-lifecycle 8s linear both;
}

@keyframes tip-lifecycle {
  0%, 24.9% { display: none;  opacity: 0; }
  25%       { display: block; opacity: 0; }       /* rendered at 2s */
  30%       { display: block; opacity: 1; }       /* faded in by 2.4s */
  95%       { display: block; opacity: 1; }
  99.9%     { display: block; opacity: 0; }       /* faded out, still rendered */
  100%      { display: none;  opacity: 0; }       /* space released */
}

/* Splash overlay: fades out after load and stops blocking clicks. */
.splash {
  display: grid;
  animation: splash-out 400ms ease-in 300ms forwards;
}
@keyframes splash-out {
  99.9% { display: grid; opacity: 0; }
  100%  { display: none; opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .tip {
    animation-name: tip-lifecycle-reduced;         /* same timing, no fades */
  }
  @keyframes tip-lifecycle-reduced {
    0%, 24.9% { display: none; }
    25%, 99.9% { display: block; }
    100% { display: none; }
  }
  .splash { animation: none; display: none; }
}

Rendering Impact: composite for the opacity fades; display flips cause one layout at the start of the visible period and one at the end. The tip costs nothing while hidden because it has no box.

With linear timing on the whole animation, keyframe percentages map directly to seconds, which keeps the sequence readable. Easing belongs on individual keyframes with animation-timing-function if the fades need curves.

The reduced-motion branch keeps the tip’s six seconds of visibility, because the tip carries information; only the fades are removed. The splash, which is decoration, is removed entirely.

Timers and classes against one keyframe animationSame sequence; one version has no script.Timers and classes against one keyframe animationsetTimeout + classesThree timers, three class changesDurations duplicated in scriptHidden tabs throttle timersFragile timing@keyframes with displayOne declaration holds the sequencedisplay flips where the fades needTiming lives in CSSSelf-contained
Same sequence; one version has no script.

Verification checklist

Constraints and trade-offs

  • Browsers without display animation support flip at the midpoint or ignore the value; test the fallback.
  • Keyframe sequences restart if the animation is re-applied, such as after a class change.
  • A hidden tab can finish a time-based animation in the background; the user may never see a timed tip.
  • Time-limited content must meet WCAG timing criteria; provide a way to keep information visible.
  • Near-100% keyframes are a workaround; comment them so they are not tidied away.

Frequently asked questions

Can display be animated in @keyframes?

Yes. It is discrete, so it flips between keyframes that set different values, at the midpoint of that interval.

How do I keep an element visible until the end of an exit animation?

Set display: block on a keyframe just before the end, such as 99.9%, and display: none on 100%, so the flip happens at the very end.

Should I use keyframes or transitions with allow-discrete?

Transitions for toggled, reversible states; keyframes for fixed, time-driven sequences that are not interrupted.

Does a timed tip meet accessibility guidelines?

Only if users can read it in time. Content that disappears automatically should be dismissible and ideally re-openable, and should not rely on timing alone.