Skip to main content
Most of TailMotion works on any element. These do not: each one animates children, so it needs the markup described here.
Treat these as optional recipes. TailMotion provides the movement; your Tailwind classes provide dimensions, colors and typography. Two of them — tm-liquid-btn and tm-hold-delete — pre-date that rule and choose their own appearance, which is noted where it applies.

View morph

One surface that reshapes between several mounted views — the Dynamic Island shape.
Child contract: every view stays mounted and carries data-tm-panel. Move data-tm-active when your component state changes, and keep aria-hidden in step with it. Supply the active view’s dimensions through --tm-view-width and --tm-view-height. Setting a fixed --tm-view-stage-width and --tm-view-stage-height makes the reshape animate a clip-path: inset() instead of layout, which is dramatically cheaper on phones. Tune the outgoing view with --tm-view-exit-scale, --tm-view-exit-scale-x and --tm-view-exit-y. The full variable list is in CSS variables.
The live blur on the outgoing panel is skipped on touch-first devices (@media (hover: none) and (pointer: coarse)), because Safari rasterizes it even for hidden panels. The spatial cross-fade is unchanged.

Avatar group

tm-avatar-more is sized and overlapped exactly like a real avatar — only its fill, ring and stacking order differ, and it sits behind the avatar before it so that avatar’s circle overlaps its edge instead of the badge clipping a real member. Variables: --tm-avatar-overlap, --tm-avatar-lift, --tm-avatar-spring, --tm-ring-width, --tm-avatar-more-bg.

Count reveal

Slot-machine style number reveal. Each character is its own child, staggered.
Child contract: one element per character. Indices are generated for the first 10; --tm-count-stagger-step sets the delay between them. To build the spans from a string, tailmotion/utils has a helper:
That is data, not DOM — render it with your framework. For vanilla JS, initCountRevealElement(element) does the DOM work.

Number swap

A number whose digits pop in together, and replay whenever the value changes — a live counter, a price, a score.
Child contract: none to write by hand — the helper re-renders one <span> per character on every update() call. Fresh nodes are what replay the pop: there is no class to toggle. Indices are also generated for the first 10, so plain markup without the helper still staggers if you split the characters yourself. --tm-number-stagger-step sets the delay between them. For React, Vue or Svelte, split the value into characters yourself and key each digit’s <span> by its slot, e.g. key={`${value}-${i}`} — remounting on value change is what replays the animation; the class only needs --tm-stagger on each child.

Streaming text

Words that resolve in one by one, left to right — for text that should read as arriving rather than as a swap.
Child contract: none to write by hand — the helper wraps each word in a <span> and keeps the whitespace between them as plain text, so the paragraph still wraps normally. --tm-stream-text-stagger-step sets the gap between words and --tm-stream-text-blur the resolve-in blur.

Text flip

Rotating words with a blur transition. CSS only — no JS required, as long as every word already exists in the markup. Render each word as a sibling *-word span and set [data-tm-count] to the word count; the container cycles through them forever on its own. Every word shares one grid cell (grid-area: 1 / 1), so they stack instead of running together with no space between them, and an infinite animation — phased per word by :nth-child — reveals one at a time. Nothing mounts, ticks, or gets torn down:
In React, Vue or Svelte this is just words.map() — no helper needed at all. 2–6 words are supported out of the box. For a vanilla project that would rather hand over a words array (or a data-tm-words attribute) than write the spans itself, initTextFlipElement renders that same markup once and gets out of the way — there is no interval running afterward, CSS owns every cycle from there:
variant: "chars" is the one case that still genuinely needs JavaScript: each word change re-splits fresh characters rather than swapping between two fixed states, which CSS keyframes cannot express. It keeps its own setInterval-driven controller (start/stop/next/prev/goTo). For React, Vue or Svelte consumers who want runtime control over a “chars” rotation — or any case with a word list that changes after mount — use createTextRotator instead. It’s a headless state machine with no DOM access, so your framework keeps ownership of rendering:

Shimmer text

A highlight that follows the shape of the glyphs. The window element sweeps one way and the copy inside counter-translates by the same amount, so the glyphs stay still on screen and the whole loop is one transform.
In React the duplication is one variable:
The element must carry no padding of its own — put it on a parent. The sweep is positioned against the padding box, and the window and the copy only stay in step while they are the same width.
The copy is aria-hidden, so the text is announced once. Writing <p class="tm-shimmer-text"> with no sweep child still works and still animates: that is the original zero-markup implementation, which repaints every frame and is scheduled for removal in 1.0. See Render cost.

Liquid button

tm-liquid-btn chooses its own fill color and, in some variants, its own line height. It pre-dates the motion-only rule and is kept for compatibility.On a <button> under Tailwind v3, preflight is unlayered and strips its colour, background and padding — see the preflight note. Set those with Tailwind classes on the same element.
Direction variants: tm-liquid-btn-left, -right, -top, -center. Speed: tm-liquid-fast, tm-liquid-slow. Color presets: tm-liquid-blue, -purple, -emerald, -amber, -rose, -cyan. Using a preset is opt-in. tm-liquid-underline and tm-liquid-wave are the non-button variants.

Icon swap

Cross-fade between two glyphs, driven by real component state.
Child contract: exactly two element children. Both stay in the DOM, so the outgoing icon animates out as cleanly as the incoming one animates in. The swapped state is read from aria-pressed, aria-expanded, aria-checked, [data-tm-swap="on"] or .tm-swapped — never a parallel flag. Grid stacking is structural, not styling: the two children share one cell so neither affects layout.

Hold to delete

tm-hold-delete chooses its own colors, padding and radius. It is kept working and unchanged, and superseded by tm-hold-confirm, which owns no appearance at all.On a <button> under Tailwind v3 it loses all three to unlayered preflight, which leaves an unpadded, uncoloured pill with the sweep still running — see the preflight note. Either set the appearance with Tailwind classes on the same element, or use tm-hold-confirm.
Variables: --tm-hold-duration, --tm-hold-color, --tm-hold-bg, --tm-hold-success, --tm-hold-success-bg, --tm-hold-fg.