Unlocking display:none Animations in Pure CSS

Animating elements from display: none to display: block was historically impossible without JavaScript hacks. Chris Coyier demonstrates how new CSS features—transition-behavior: allow-discrete and @starting-style—make it feasible today, especially for native elements like <dialog> and popovers. The core challenge: discrete properties like display don't interpolate (no in-between states like display: 50%), so browsers snap them instantly. allow-discrete delays the flip until transition end, enabling smooth opacity, translate, or scale effects during the change.

Tradeoffs are real: keywords like allow-discrete and overlay feel arbitrary and hard to remember. Source order matters critically—misplace styles and specificity breaks everything. No JS needed, but debugging requires precise setup. Results: production-ready fades, slides, and rotations without hacks, outperforming old checkbox or :target tricks.

The 3-2-1 Styling Pattern: Source Order Is King

Coyier introduces a "3-2-1" authoring model for reliable in-out animations:

  1. State 3: On-the-way-out styles (e.g., dialogue:not([open]) or equivalent)—applied during close transition.
  2. State 2: Open styles (e.g., dialogue:open)—final visible state.
  3. State 1: On-the-way-in styles (@starting-style for :open)—initial render state before first paint.

Write in reverse order (3-2-1) due to CSS cascade: @starting-style has low specificity, so it must follow open styles to override. Users experience 1-2-3, but authoring 3-2-1 prevents footguns.

Example for fade + slide on <dialog>:

/* State 3: Out styles */
dialogue:not([open]) {
  transform: translateX(100%);
  opacity: 0;
  transition: all 0.3s;
}

/* State 2: Open */
dialogue:open {
  transform: translateX(0);
  opacity: 1;
  transition: opacity 0.3s, transform 0.3s, display;
  transition-behavior: allow-discrete display;
}

/* State 1: In styles (overrides open for first render) */
@starting-style { dialogue:open { opacity: 0; transform: translateX(-100%); } }

This slides in from left, out to right—mismatched motions feel satisfying. Scale to any animatable property (opacity, transform, background). Fails without allow-discrete: display flips upfront, snapping instantly.

"The order though does matter because starting style... has no specificity... it's better to always put them after your open styles because they need to override your open styles." — Chris Coyier, explaining why 3-2-1 prevents cascade breakage (key to avoiding subtle bugs).

Dave Rupert's "minimum viable design system" pen struggled here: wrong order broke half the animations, underscoring real-world pitfalls.

Native Dialogues: Top-Layer Magic Without Z-Index Wars

<dialog> defaults to display: none when closed—perfect for in-out demos. Perks:

  • Top layer: Renders above everything, no z-index fights or DOM positioning.
  • Free focus trapping: No custom JS.
  • HTML-only control: <button popovertarget="id"> or dialog.showModal().

From closed (display: none, inaccessible) to open: animate via :open pseudo-class. Backdrop (::backdrop) fades separately but follows same 3-2-1 + overlay keyword on parent:

dialogue::backdrop {
  background: linear-gradient(45deg, #333, #666);
  opacity: 1;
  transition: opacity 0.3s;
}

/* On parent */
transition-behavior: allow-discrete display overlay;

Without overlay, backdrop snaps. CSSWG discussed removing it—fingers crossed. Demo: Full modal with diagonal gradient backdrop, sliding content, Grammarly popup interruption for realism.

"Imagine if all those CSS crimes could use dialogue and popovers... the possibilities are much stronger." — Coyier on ditching checkbox hacks and <details> for native APIs (highlights migration path from hacks).

Popovers: Attribute-Driven, Crime-Worthy Flexibility

Popovers mirror dialogues but use popover attribute + popovertarget for invokers. No DOM structure needed—elements anywhere communicate via IDs. Ideal for tooltips, menus.

Same 3-2-1 applies, plus anchor positioning (implied anchor from invoker). Example: Inline notebook tooltip rotates in:

/* Similar 3-2-1 with rotate */
[popover]:not([open]) { transform: rotate(-10deg); }
[popover][open] { transform: rotate(2deg); }
@starting-style { [popover][open] { transform: rotate(-90deg); opacity: 0; } }

Click-away closes. Tradeoff: Slightly different UA styles vs. dialogues (see Coyier's post, linked in slides). Anchor positioning auto-handles placement.

"This has no such restrictions crime away crime it up." — On popovers' loose coupling beating <details> or + selectors (fun nod to hacky patterns).

Backdrop and Edge Cases: Keywords You Must Memorize

Backdrops need transition-behavior: allow-discrete display overlay on parent, not ::backdrop itself. Animate opacity/background there too. No transition: all—specify properties explicitly.

Edge learnings:

  • First render uses @starting-style—prevents instant snap to open state.
  • Works for custom divs too (add/remove .open class toggling display).
  • Source order trips up even pros (Rupert's pen).

Full demo pen: Clean fonts, custom dialog styling, left-in/right-out slide, gradient backdrop.

"There's a lot to get wrong... it's cool that we can do it... I want you to understand it all." — Summing the quirky but powerful API (candid on complexity vs. value).

Key Takeaways

  • Always use 3-2-1 source order: out styles > open > @starting-style in, to beat specificity.
  • transition-behavior: allow-discrete display delays discrete flips—essential for no-JS.
  • <dialog> and popovers handle modals/tooltips natively; migrate from hacks.
  • Backdrops: overlay keyword on parent + ::backdrop opacity/gradients.
  • Test first render: @starting-style ensures in-animation starts faded/off-screen.
  • Specify transitions explicitly; add overlay for top-layer backdrops.
  • Mismatch in/out motions (e.g., left-in, right-out) for polish.
  • Bookmark pens/slides—allow-discrete/overlay are forgettable but mandatory.
  • No z-index/DOM fights: top-layer FTW for overlays.