Copy-paste motion for real apps — page fades, scroll reveals, list springs,
modal physics, and now the component moments too: toasts, sliding tabs, accordions,
tooltips, error shake, and skeleton crossfade. Two files, zero dependencies, CSP-safe.
Every recipe respects prefers-reduced-motion, and every component
rides its own shadow — no global dim.
This page just did it. Put .tx-page on your top wrapper —
the whole view fades and rises in on load. On supporting browsers it also opts into the
native cross-document View Transitions API.
<!-- one class, that's the whole recipe -->
<body class="tx-page"> … </body>
Scroll the cards into view — each fades up a beat after the last.
Wrap them in data-tx-stagger, tag each with tx-reveal.
transitions.js numbers them and reveals via IntersectionObserver. No JS to write.
--tx-i × 60ms.tx-scale to grow from 94%.tx-left or tx-right.<div data-tx-stagger>
<div class="tx-reveal">…</div>
<div class="tx-reveal tx-scale">…</div>
</div>
/* or drive it yourself: */
Transitions.reveal(".tx-reveal", { stagger: 60 });
Same stagger, springier curve. tx-spring pops each row in
with a touch of overshoot — good for freshly loaded lists and search results.
<ul data-tx-stagger> <li class="tx-spring">…</li> </ul>
Backdrop fades, panel springs up and scales in. Open with a
data-tx-open button; close on the ✕, the scrim, or Esc.
Focus moves in and returns on close. All wiring is in transitions.js — the markup is declarative.
<button data-tx-open="demo-modal">Open</button>
<div id="demo-modal" class="tx-modal" aria-hidden="true">
<div class="tx-modal__scrim"></div>
<div class="tx-modal__panel">
… your content …
<button data-tx-close>Close</button>
</div>
</div>
Tangible cards that rise, fade, and scale into a corner region, then leave the
instant they expire. They stack, they carry an optional action, and they ride their own shadow —
no backdrop, no dim. All wiring is declarative via data-tx-toast.
<!-- declarative --> <button data-tx-toast="Saved." data-tx-toast-tone="success">Save</button> /* or from JS */ Transitions.toast("Draft saved.", { tone: "info", action: "Undo", onAction: undo });
A pill glides under the active tab and resizes to fit it; the matching panel fades in.
Arrow keys, Home/End, focus roles — all handled. Mark up data-tx-tabs and
transitions.js does the measuring.
<div data-tx-tabs>
<div class="tx-tabs__list">
<span class="tx-tabs__pill"></span>
<button data-tx-tab="fonts" aria-selected="true">Fonts</button>
<button data-tx-tab="motion">Motion</button>
</div>
<div data-tx-panel="fonts">…</div>
</div>
Panels animate their real height and settle to auto so they stay
responsive; the chevron rotates in step. Add data-tx-accordion="single" to keep
just one open at a time.
<div data-tx-accordion="single">
<div class="tx-acc__item is-open">
<button data-tx-acc-trigger>Title <span class="tx-acc__chevron">▸</span></button>
<div class="tx-acc__panel">…</div>
</div>
</div>
Hover or focus and the tip appears after a short dwell; move away and it's gone at once —
the asymmetry that makes tooltips feel intentional, not twitchy. One shared bubble, positioned with
data-tx-tip-pos.
<button data-tx-tooltip="Appears after a dwell" data-tx-tip-pos="top">Hover</button>
/* auto-wired; or: Transitions.tooltip(el, "text", { delay: 380 }) */
A short, damped shake on its own cubic-bezier — the universal
"that input was rejected" gesture. One-shot; it strips itself on animationend
so it replays cleanly every time.
<input id="pw">
<button data-tx-shake="#pw">Submit</button>
/* or on a failed submit: */
if (!valid) Transitions.shake("#pw");
A shimmering placeholder dissolves into the real content the moment it lands — ghost fades out as content fades in. The shimmer holds still under reduced motion.
<div class="tx-skel" id="card">
<div class="tx-skel__ghost"></div>
<div class="tx-skel__real">… real content …</div>
</div>
/* when data arrives: */
Transitions.ready("#card");