AnimatePresence wrapper.
The state contract
Put the state on the element itself.An element carrying none of these is treated as open. That is deliberate: a
missing or misspelled attribute leaves content visible rather than invisible.
The failure mode of a typo should never be a blank screen.
The vocabulary
Five classes, on purpose. A presence library with twenty entrance styles is a keyframe collection wearing a different hat.tm-presence-fade is the safest default: nothing moves, so it composes with
any layout and never fights a positioned popper.
tm-presence-pop is the only one that overshoots, and the overshoot lives in
the easing rather than a keyframe, so it stays interruptible. A calm profile —
or --tm-overshoot: 0 — flattens it back to a decelerating curve.
Closed motion is quieter by design: the exit runs at 70% of the open duration
(--tm-presence-exit-scale) on a weaker curve. The user has already decided;
the exit should get out of the way rather than perform.
Direction is logical
On the inline axis these mirror themselves in right-to-left contexts — “start”
is the left edge in LTR and the right edge in RTL. On the block axis they do
not, because up is up in every writing direction.
Measured, in a real right-to-left document:
Interruptible by construction
These are transitions, not keyframes. Reversing state halfway through retargets from the current position instead of snapping back to the start. Closing atm-presence-slide-block panel and reversing at the midpoint:
The mounting contract
TailMotion ships no JavaScript to mount or unmount anything — that is your framework’s job, and every framework already has a tool for it. Three ways to satisfy the contract, in order of preference:- Keep it mounted. For a menu, a tooltip or an accordion this is usually correct anyway, and it is free.
- Use your library’s own presence primitive. Radix UI, Base UI, Ark and
Melt all keep content mounted for the duration of the exit and set
data-statefor you. The class is the entire integration. - Delay the unmount past the closed duration. See Framework integration.
What closed actually means
The closed state ends onvisibility: hidden, not just opacity: 0. That
matters more than it sounds:
- A fully transparent element still swallows clicks.
pointer-events: noneandvisibility: hiddenboth stop that. - A fully transparent element is still read by a screen reader.
visibility: hiddenremoves it from the accessibility tree.
visibility interpolates discretely at the end of a transition, so the element
stays visible for the whole exit and only disappears once the animation is
finished. It keeps its layout box, which is inherent to “keep it mounted” — for
a panel in normal flow, position it or use a recipe like
tm-accordion-panel that animates the box
itself.
First paint
An element mounted already open has no previous state to transition from, so without help it would simply appear.@starting-style supplies the missing
“before” frame, and TailMotion declares it for every presence class.
@starting-style, the element is visible immediately —
correct, just not animated. See Browser support.
Transform safety
Every presence class animatesopacity, translate and scale — never
transform. A Tailwind rotate-3, -translate-y-1 or skew-x-2 on the same
element survives: