Skip to main content
Your component already knows whether the menu is open. Presence reads that state rather than asking for a second source of truth.
Toggle the attribute and the panel animates both ways. There is no class to swap, no runtime to install, and no 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 a tm-presence-slide-block panel and reversing at the midpoint:
The browser hands the in-flight value to the new transition. No JavaScript is involved, and no velocity is lost to a restart.

The mounting contract

A closed element must stay in the DOM until its closed transition finishes. If your framework unmounts it the moment state flips, there is nothing left to animate and the panel simply disappears.
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:
  1. Keep it mounted. For a menu, a tooltip or an accordion this is usually correct anyway, and it is free.
  2. 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-state for you. The class is the entire integration.
  3. Delay the unmount past the closed duration. See Framework integration.

What closed actually means

The closed state ends on visibility: hidden, not just opacity: 0. That matters more than it sounds:
  • A fully transparent element still swallows clicks. pointer-events: none and visibility: hidden both stop that.
  • A fully transparent element is still read by a screen reader. visibility: hidden removes 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.
Where the browser has no @starting-style, the element is visible immediately — correct, just not animated. See Browser support.

Transform safety

Every presence class animates opacity, translate and scale — never transform. A Tailwind rotate-3, -translate-y-1 or skew-x-2 on the same element survives:

Tuning