Skip to main content
Everything in TailMotion is built from the same four groups of custom properties. Learn the groups once and every class becomes predictable.

The four token groups

1

Explicit overrides

--tm-duration, --tm-delay, --tm-easing, --tm-iteration-count, --tm-exit-duration.Deliberately unset, so each motion class keeps its own tuned default. Set them per element with tm-duration-*, tm-delay-*, tm-ease-*, tm-repeat-*, the Tailwind plugin, or an inline style. An explicit value always wins.
2

Profile factors

--tm-duration-scale, --tm-emphasis, --tm-overshoot.Inherited multipliers. A motion personality retunes a whole subtree by changing these, and each animation keeps its relative character because it multiplies its own default rather than being replaced by a global one.
3

Shared scalars

--tm-distance, --tm-stagger-step.One value each, already shared by every class that uses them, so a profile sets them outright rather than scaling them.
4

Role easings

--tm-ease-entrance, --tm-ease-exit, --tm-ease-interaction, --tm-ease-morph, --tm-ease-emphasis.Classes opt into a role; a profile retunes the role. Animations with a character of their own — linear spins, ambient drifts — stay out of this and keep their literal curve.

How a class reads its duration

Every animation in the library is written this way:
Read it right to left:
  • With nothing set, the class runs at its own tuned default — 260ms here.
  • An inherited profile changes --tm-duration-scale, so the class scales. tm-pop at 380ms and tm-fade-in at 250ms stay in proportion.
  • tm-duration-300 sets --tm-duration on the element, which replaces the whole calc(). Exactly 300ms, not 300ms times anything.
:root never carries a value for --tm-duration, --tm-delay, --tm-easing or --tm-iteration-count. A root value would shadow every per-class default in the library — which is exactly the bug 0.7 fixed, and which npm run check now guards against.

The override order

From weakest to strongest: Profiles are plain inherited custom properties, so “nearest wins” is just CSS inheritance — there is no resolution logic to learn. Element-level utilities beat a profile on the same element too, because they are declared later in the stylesheet than the profile classes and carry !important when generated by the plugin.

Easing roles

Entrances and exits both use an ease-out family. An ease-in exit delays the visual feedback and reads as sluggish; the exit is made quieter by being shorter and travelling less, not by starting slowly. tm-ease-* on an element overrides whatever role a class uses.

Emphasis and overshoot

Two separate factors, because “smaller” and “no bounce” are different requests.
tm-motion-calm sets --tm-overshoot: 0 and --tm-emphasis: 0.55: the pop still grows into place, it just stops springing.

Which one do I want?

Keyframes or transitions?

The distinction decides whether a class can be interrupted. That is why presence exists as its own category rather than being a pair of entrance and exit keyframes: a menu the user changes their mind about halfway has to reverse from where it is, not snap back to where it started.
Two keyframe classes cannot be applied to the same element — whichever comes later in the stylesheet wins animation-name, not both. That is a property of CSS animations, not of this library. Transitions do stack, so tm-press and tm-hover-lift together are fine. See Transform safety.