Purposeful CSS motion that speaks Tailwind.
Add tuned keyframes and interruptible transitions with composable classes and no runtime in the core stylesheet.
Documentation · Quickstart · Class reference · Live demo · Animation explorer · Feedback · Contributing
npm install tailmotion@import "tailmotion/css";<button class="tm-press rounded-lg bg-blue-600 px-4 py-2 text-white">
Save changes
</button>TailMotion gives product interfaces a tested motion language with good decisions already made. Tailwind owns layout, color, spacing, radius, shadows and typography; TailMotion owns how an element enters, exits, responds, repeats and reacts to state.
A keyframe collection answers "how do I fade this in?" TailMotion answers "how should this whole product move, and what happens when the state changes?"
📖 Full documentation → · 🎛️ Live capability demo →
Low-level animation utilities, like tw-animate-css, let you construct an
entrance or exit from parameters:
<!-- Compose the animation from parameters -->
<div class="animate-in fade-in slide-in-from-top-8 duration-500">
Content
</div>TailMotion gives you the tuned behavior instead of the parameters:
<!-- Choose the behavior -->
<div class="tm-slide-block-start tm-duration-500">
Content
</div>Reach for parameter-based utilities when you want to build a custom animation by hand. Reach for TailMotion when you want a named, product-tested motion behavior with a sensible default. The two solve different levels of the same problem, and nothing stops you from using both in one project.
<main class="tm-motion-calm">
<button class="tm-press">Save</button>
<div class="tm-slide-block-start">Saved</div>
</main>tm-motion-calm, tm-motion-productive and tm-motion-expressive retune every
TailMotion descendant. Duration is a factor, not a value, so each animation
keeps its own relative character — tm-pop stays livelier than tm-fade-in in
all three. Element-level utilities like tm-duration-300 still win.
<div class="tm-presence-slide-block" data-state="open">Product menu</div>Reads data-state, aria-expanded, aria-pressed, aria-checked or
.tm-open / .tm-closed. Built from transitions, so reversing mid-flight
retargets from the current position instead of restarting. Radix UI, Base UI,
Ark and Melt work with no adapter — the class is the integration.
<dialog class="tm-native-dialog">…</dialog>
<div popover class="tm-native-popover">…</div>
<details class="tm-native-disclosure">…</details>The browser keeps focus management, the top layer, light dismiss, Escape and every native semantic. TailMotion adds the movement, picks no color or size, and adds no JavaScript.
<div role="status" data-state="open" class="tm-toast">Changes saved</div>
<button data-state="loading" class="tm-feedback-button">
<span class="tm-feedback-idle">Save</span>
<span class="tm-feedback-loading">Saving</span>
<span class="tm-feedback-success">Saved</span>
</button>Menu, dialog, toast, tooltip, accordion, tabs, loading-to-success and hold-to-confirm. State selectors, timing, easing and transform origin — no colors, no dimensions, no components.
<section class="tm-scroll-reveal">Content</section>CSS view timelines, driven by the browser on the compositor. Entirely inside
@supports (animation-timeline: view()), so a browser without support renders
ordinary, fully visible content — a fallback an IntersectionObserver cannot
offer.
<ul data-state="open" class="tm-stagger tm-stagger-75 tm-stagger-from-end">
<li>Profile</li><li>Settings</li><li>Sign out</li>
</ul>Enter in document order, exit last-item-first, replay from a state change — with nothing reordered in the DOM, so reading order, tab order and the accessibility tree are untouched.
Every one of these is verified by npm run check, which fails the build if it
stops being true.
- Zero runtime in the CSS core. No JavaScript, no observer, no frame loop.
- Nothing modern can hide content. A hidden base state is only ever declared
inside the
@supportsblock that also says how it comes back. - Reduced motion preserves the state. Animations collapse to 1ms rather than being removed, with two deliberate, documented exceptions.
- Direction is logical. Inline-axis motion mirrors in RTL; block-axis motion does not.
- Transform-safe. 39 keyframes — every slide, scale, zoom, fade, presence,
scroll and stagger behaviour — animate
translate/scale/rotaterather than thetransformshorthand, so a Tailwindrotate-3on the same element survives. The 36 that keep the shorthand need it (3D flips needperspective(); a few scale before they translate) and are each named with a wrapper workaround. The build fails if either set changes without the documentation. - Continuous motion never repaints. Every looping effect in the core runs on the compositor, checked by the build. The exceptions are two decorative background families and one deprecated path, each named in Render cost.
- Tailwind v3, Tailwind v4, or no Tailwind at all.
- Usage-generated CSS for the simple catalogue (experimental). Tailwind v3
(via the plugin) and Tailwind v4 (via
tailmotion/tailwind.css) can emit only the fade/pop/bounce-style utilities and interactions your markup actually uses, instead of the whole catalogue. See Installation.
| Quickstart | Install and ship the first four behaviours |
| Installation | Tailwind v3, v4, the CDN, the optional plugin |
| The motion model | Four token groups and one override order |
| Class reference | Every class, trigger, state, duration and contract |
| CSS variables | Every --tm-* property and its default |
| Browser support | Every guarded feature and its exact fallback |
| Accessibility | Reduced motion, RTL, focus, and what motion must never be alone |
| Framework integration | React, Vue, Svelte, HTML, headless UI |
| Migration | Upgrading to 0.8 |
| Support and non-goals | What is supported, and what will never be built |
The docs/ folder in this repository is the source for all of the above.
Motion personalities
| Profile | Use for | Duration | Travel | Overshoot |
|---|---|---|---|---|
tm-motion-calm |
Settings, finance, long-form reading | ×1.1 | 8px | none |
tm-motion-productive |
The recommended default for product UI | ×0.85 | 10px | ×0.6 |
tm-motion-expressive |
Onboarding, marketing, celebration | ×1.2 | 22px | ×1.3 |
Plus tm-speed-*, tm-emphasis-* and tm-no-overshoot for one axis at a time.
Presence and recipes
tm-presence-fade · tm-presence-scale · tm-presence-pop ·
tm-presence-slide-block · tm-presence-slide-inline
tm-menu · tm-dialog · tm-dialog-backdrop · tm-toast · tm-tooltip ·
tm-accordion-panel · tm-tab-panel · tm-tab-indicator ·
tm-feedback-button · tm-hold-confirm
tm-native-popover · tm-native-dialog · tm-native-disclosure
Entrances, exits and interactions
Entrances tm-fade-in · tm-scale-in · tm-slide-block-start ·
tm-slide-block-end · tm-slide-inline-start · tm-slide-inline-end ·
tm-blur-in · tm-pop · tm-drop · tm-zoom-in · tm-zoom-out ·
tm-rotate-in · tm-elastic · tm-reveal · tm-unfold · tm-glide ·
tm-scale-fade · tm-rise · tm-flip-x · tm-flip-y · tm-swing-in
Exits tm-fade-out · tm-scale-out · tm-slide-block-out ·
tm-slide-inline-out · tm-blur-out
Interactions tm-press · tm-hover-lift · tm-hover-scale ·
tm-rotate-hover · tm-rotate-press · tm-icon-swap
Continuous tm-spin · tm-pulse · tm-bounce · tm-float · tm-drift ·
tm-sway · tm-glow · tm-morph · tm-ripple · tm-shimmer · tm-sparkle
Timing tm-duration-* · tm-delay-* · tm-ease-* · tm-repeat-* ·
tm-distance-* · tm-stagger-*
Full details, including every duration and easing role, in the class reference.
Modular imports
@import "tailmotion/css"; /* everything — 35.3 KB gzipped */
@import "tailmotion/profiles.css"; /* 4.0 KB */
@import "tailmotion/presence.css"; /* 5.1 KB */
@import "tailmotion/native.css"; /* 5.3 KB */
@import "tailmotion/recipes.css"; /* 6.7 KB */
@import "tailmotion/scroll.css"; /* 4.4 KB */
@import "tailmotion/choreography.css"; /* 5.1 KB */
@import "tailmotion/animations/base.css"; /* or one family at a time */
@import "tailmotion/animations/fade.css";The full bundle is not tree-shaken, and each module repeats the shared token
layer — so if you need more than one, import tailmotion/css instead.
Not supported. TailMotion is CSS and DOM selectors. React Native Web can use it for its browser target only, where the output is real DOM and real CSS.
- Read CONTRIBUTING.md before opening a substantial change.
- Participation is governed by the TailMotion Code of Conduct.
- Report bugs and propose features through GitHub Issues.