[popover], <dialog> and <details> are the three elements the browser
already handles correctly — focus, the top layer, light dismiss, Escape,
keyboard semantics — and the three that were historically impossible to animate
out, because they end in display: none.
Modern CSS fixed that. TailMotion packages the fix.
TailMotion picks no color, dimension, padding, radius or shadow for any of
these, and adds no JavaScript. It animates the panel, the backdrop’s opacity,
and the disclosure’s height. Everything else stays yours and the browser’s.
Popover
Set
--tm-origin to the corner the popover grows from. For an anchored menu
that is the trigger’s edge, which stops the panel from appearing to inflate out
of its own middle.
The exit works because display and overlay are named in
transition-property with transition-behavior: allow-discrete. Verified in
Chrome: on close the element keeps display: block for the full 140ms and only
then becomes none.
Dialog
The dialog and its backdrop move as one unit, so they share a duration and an
easing. That is the “paired elements” rule: things that move together must feel
like one thing.
A closed
<dialog> is display: none in the UA stylesheet, so the transparent
base state can never strand visible content.
Disclosure
1
A content entrance, everywhere
<details> only renders its content while open, so a plain keyframe is
enough — no height has to be measured. This runs in every browser, including
Safari and Firefox today.2
A height interpolation, where supported
Where
::details-content and interpolate-size exist, the box height
interpolates as well, so the surrounding page reflows smoothly instead of
jumping. TailMotion switches the content entrance off in that case, so the
movement is never doubled.summary::marker cannot be transformed, so marker rotation needs an element you
own: add data-tm-marker to any child of the <summary>. It rotates 90° on
open; --tm-marker-rotate changes the angle.
For a div-based accordion — the shape most headless libraries produce — use
tm-accordion-panel instead. It animates a
0fr → 1fr grid row, which works everywhere and needs no measurement.Support, and what happens without it
Every modern rule sits behind an@supports guard, and the hidden base state is
only ever declared inside the guard that also says how it comes back. No browser
is left with invisible content.
The guard for both discrete transitions and
@starting-style is
@supports (transition-behavior: allow-discrete), because the two shipped
together in Chrome and Firefox. Safari shipped them one point release apart, so
in Safari 17.4 exactly the exits animate and a panel mounted already-open
appears instead of animating in. Nothing breaks; one frame is missing.What stays with the browser
Worth being explicit, because this is the reason to use native elements at all:- Focus.
showModal()moves focus into the dialog and restores it on close. - The top layer. No
z-indexarms race. - Light dismiss. Clicking outside a popover closes it.
- Escape. Works without a key handler.
- Semantics.
<details>is a real disclosure to a screen reader;<dialog>is a real dialog.