Star203

Documentation

Introduction

Motionwind v2 — a shared utility language for web animations. Write animate-* classes, get Motion output at build time.

Motionwind v2

Motionwind is an animation utility language for web applications. It lets you write animation instructions as Tailwind-like CSS classes and converts them into framework-specific animation code at build time.

// You write this (Tailwind-like classes):
<button className="animate-hover:scale-110 animate-tap:scale-90 animate-focus:scale-105 animate-spring">
  Hover, press, or focus me
</button>

// The compiler produces this (Motion component):
<motion.button
  whileHover={{ scale: 1.1 }}
  whileTap={{ scale: 0.9 }}
  whileFocus={{ scale: 1.05 }}
  transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
  Hover, press, or focus me
</motion.button>

No animation code ships in className. Ordinary CSS and Tailwind classes remain unchanged. The compiler adds the Motion import and "use client" directive automatically.

What Motionwind is

  • A class-based syntax for defining hover, tap, focus, scroll, drag, enter, exit, and layout animations.
  • A compile-time compiler that transforms animate-* classes into Motion props for React, Vue, or vanilla JavaScript.
  • A runtime fallback (mw.*) for dynamic class names that cannot be compiled.
  • A configuration system (motionwind.config.ts) for animation tokens, presets, strict diagnostics, and reduced-motion policy.
  • A shared core (motionwind-core) that all frameworks use — one parser, one set of types, one intermediate representation (ParsedResult).

What Motionwind is not

  • It does not replace Motion. Use Motion directly for complex timelines, imperative control, or custom motion values.
  • It does not require Tailwind CSS. The animate-* classes are independent. Other CSS passes through untouched.
  • It does not add runtime overhead on the compile path. Static classes are resolved at build time.

Supported frameworks

FrameworkPackageStatusHow it works
React, Next.js, Vitemotionwind-reactStableBabel/Vite/Next.js compile-time transform
Vue, Nuxtmotionwind-vueStableTemplate compile-time transform
Vanilla JavaScriptmotionwind-vanillaStableDOM runtime backed by Motion
React Native, Expomotionwind-react-nativeBetaRuntime backed by Reanimated

All frameworks share the same animate-* syntax. You can switch frameworks without rewriting animation classes.

The grammar

Every Motionwind class follows this pattern:

animate-{trigger}:{property}-{value}

Triggers (what causes the animation):

TriggerMeaningMotion prop
hoverMouse/finger hovers over elementwhileHover
tapMouse click or finger presswhileTap
focusElement receives keyboard focuswhileFocus
inviewElement enters the viewportwhileInView
dragElement is being draggedwhileDrag
initialStarting state (before animation)initial
enterTarget state (after mount)animate
exitState when element unmountsexit

Properties (what animates):

PropertyCSS/TransformExample values
scaletransform: scale()105, 90, [1,1.2,1]
rotatetransform: rotate()45, -10
xtransform: translateX()20, -100
ytransform: translateY()20, -100
opacityopacity0, 100, 50
blurfilter: blur()4, 10

Transition (how the animation runs):

ClassEffect
animate-duration-200Duration in milliseconds
animate-delay-100Delay before starting
animate-springSpring physics (default)
animate-spring-bouncyBouncy spring preset
animate-stiffness-300Spring stiffness
animate-damping-20Spring damping
animate-ease-in-outEasing function

Full examples:

// Button with hover scale, tap scale, spring transition
<button className="animate-hover:scale-110 animate-tap:scale-90 animate-spring">
  Click me
</button>

// Fade-in element with stagger
<div className="animate-initial:opacity-0 animate-enter:opacity-100 animate-duration-300 animate-delay-100">
  Appears after 100ms
</div>

// Scroll-triggered animation
<section className="animate-inview:opacity-100 animate-inview:y-0 animate-initial:opacity-0 animate-initial:y-20 animate-once">
  Fades in when scrolled into view
</section>

How the compiler works

When you write:

<button className="px-4 py-2 animate-hover:scale-110 animate-tap:scale-95 animate-duration-200">
  Click me
</button>

The compiler:

  1. Reads the className string at build time.
  2. Extracts animate-hover:scale-110, animate-tap:scale-95, animate-duration-200.
  3. Removes those animation tokens from className (leaving px-4 py-2).
  4. Changes <button> to <motion.button>.
  5. Adds whileHover={{ scale: 1.1 }}, whileTap={{ scale: 0.95 }}, transition={{ duration: 0.2 }}.
  6. Injects import { motion } from "motion/react" and "use client".

The result is a standard Motion component. No runtime parser ships to the browser.

Dynamic class names

Static classes are compiled. Dynamic animation values need the runtime:

// Static — compiled (no runtime overhead)
<button className="animate-hover:scale-110">Static</button>

// Mixed — animation token compiled, CSS is dynamic
<button className={`animate-hover:scale-110 ${active ? "is-active" : ""}`}>
  Mixed
</button>

// Dynamic animation — use mw.* runtime
<mw.button className={`animate-hover:scale-${large ? "120" : "105"}`}>
  Dynamic
</mw.button>

mw.* components parse animate-* classes at runtime and pass the result to Motion. Use them when the animation token or value is computed from props or state.

Design-system components

Wrap your own components with mw.create():

import { mw } from "motionwind-react";
import { Button } from "./button";

const MotionButton = mw.create(Button);

<MotionButton className="animate-hover:scale-105 animate-tap:scale-95">
  Save
</MotionButton>;

The wrapped component must forward its ref and className to a DOM element.

Configuration

Create motionwind.config.ts at your project root:

import { defineConfig } from "motionwind-core";

export default defineConfig({
  adapter: "react",
  strict: true,
  reducedMotion: "user",
  tokens: {
    durations: { fast: 160, deliberate: 480 },
    easings: { product: [0.22, 1, 0.36, 1] },
    springs: { responsive: { stiffness: 420, damping: 28 } },
  },
  presets: {
    "card-enter":
      "animate-initial:opacity-0 animate-enter:opacity-100 animate-duration-deliberate",
  },
});

Tokens become classes: animate-duration-fast, animate-ease-product, animate-spring-responsive, animate-preset-card-enter.

Packages

PackagePurposeTier
motionwind-coreParser, types, config, registry, plugin contractsStable
motionwind-reactReact runtime + Babel, Vite, Next.js integrationsStable
motionwind-vueVue components + Vite integrationStable
motionwind-vanillaJavaScript runtimeStable
motionwind-react-nativeExpo/React Native runtimeBeta
motionwind-unpluginVite, Rollup, webpack, Rspack, esbuild layerStable
create-motionwindInit, doctor, migrate, preset CLIStable
eslint-plugin-motionwindLint diagnosticsStable
prettier-plugin-motionwindAuto-formattingStable
motionwind-mcpAI/LLM tools (validation, explanation, generation)Stable

Compatibility

CI validates every framework against the same syntax corpus. Unsupported features produce actionable diagnostics, never silent failures.

FrameworkTierGesturesScrollLayoutDragVariantsSVGReduced motionSSR
Next.jsStablePassPassPassPassPassPassPassPass
React + ViteStablePassPassPassPassPassPassPassN/A
Vue + ViteStablePassWarningPassPassPassPassPassN/A
NuxtStablePassWarningPassPassPassPassPassPass
Vanilla JavaScriptStablePassPassUnsupportedUnsupportedUnsupportedPassPassN/A
Expo / React NativeBetaBetaBetaWarningWarningBetaWarningPassN/A

"Warning" means the adapter detects the syntax, explains the gap, and leaves direct Motion or platform APIs available.