Star203

Documentation

Getting Started

Step-by-step guide to using Motionwind — from first class to production deployment.

Getting Started

This guide walks you through using Motionwind in a real project. By the end you'll have animated components working with zero runtime parser overhead.

Step 1: Install

Run the initializer. It detects your framework, installs packages, and creates motionwind.config.ts:

npx create-motionwind@2 init

Preview changes first with --dry-run:

npx create-motionwind@2 init --dry-run

If auto-detection fails, specify your framework:

npx create-motionwind@2 init --framework=next      # Next.js
npx create-motionwind@2 init --framework=react      # React + Vite
npx create-motionwind@2 init --framework=vue        # Vue + Vite
npx create-motionwind@2 init --framework=vanilla    # Vanilla JS
npx create-motionwind@2 init --framework=react-native  # Expo / React Native

Step 2: Add the build integration

Each framework has a specific integration. The CLI does this automatically, but here's the manual setup:

Next.js — wrap your config:

// next.config.mjs
import { withMotionwind } from "motionwind-react/next";
import motionwindConfig from "./motionwind.config";

export default withMotionwind({}, motionwindConfig);

React + Vite — add the plugin:

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import motionwind from "motionwind-react/vite";
import motionwindConfig from "./motionwind.config";

export default defineConfig({
  plugins: [motionwind(motionwindConfig), react()],
});

Vue + Vite — add the transform and plugin:

// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import {
  createMotionwindTransform,
  MotionwindPlugin,
} from "motionwind-vue/vite";
import motionwindConfig from "./motionwind.config";

export default defineConfig({
  plugins: [
    createMotionwindTransform(motionwindConfig),
    vue(),
    MotionwindPlugin(motionwindConfig),
  ],
});

Vanilla JavaScript — call the initializer:

import { motionwind } from "motionwind-vanilla";
import motionwindConfig from "./motionwind.config";

motionwind({ config: motionwindConfig });

React Native — wrap your app with the provider:

import { MotionwindProvider } from "motionwind-react-native";
import motionwindConfig from "./motionwind.config";

export default function App() {
  return (
    <MotionwindProvider config={motionwindConfig}>
      <YourApp />
    </MotionwindProvider>
  );
}

Step 3: Write your first animation

Add animate-* classes to any element:

<button className="animate-hover:scale-110 animate-tap:scale-90 animate-spring">
  Hover or press me
</button>

That's it. The compiler transforms this into a Motion component with whileHover, whileTap, and transition props. No animation code stays in className.

Step 4: Verify

Run the diagnostic to confirm everything is wired up:

npx create-motionwind@2 doctor

Test hover, tap, and focus in your browser:

<button
  class="animate-hover:scale-110 animate-tap:scale-90 animate-focus:scale-105 animate-spring"
>
  Test Motionwind
</button>

How the compile path works

When you write:

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

The build step:

  1. Finds animate-hover:scale-110 and animate-tap:scale-95 in the className.
  2. Removes those tokens (leaving px-4 py-2).
  3. Changes <button> to <motion.button>.
  4. Adds whileHover={{ scale: 1.1 }} and whileTap={{ scale: 0.95 }}.
  5. Injects import { motion } from "motion/react" and "use client".

The result is a standard Motion component. No parser runs in the browser.

Static vs dynamic classes

Static classes (string literals, template literals with known values) are compiled at build time:

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

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

Dynamic classes (animation values computed at runtime) need the mw.* runtime:

import { mw } from "motionwind-react";

// Dynamic — animation value comes from a prop
<mw.button className={`animate-hover:scale-${large ? "120" : "105"}`}>
  Dynamic
</mw.button>;

mw.* parses animate-* classes at runtime and passes the result to Motion. Use it only when the animation token or value is truly dynamic.

Working with 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 basics

The CLI creates motionwind.config.ts at your project root:

import { defineConfig } from "motionwind-core";

export default defineConfig({
  adapter: "react", // or "vue", "vanilla", "react-native"
  strict: true, // error on unknown classes in CI
  reducedMotion: "user", // respect OS preference (recommended)
  tokens: {
    durations: { fast: 160, deliberate: 480 },
    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-spring-responsive, animate-preset-card-enter.

When to use classes vs Motion directly

Use animate-* classes forUse Motion directly for
Hover, tap, focus effectsComplex sequenced timelines
Mount/unmount transitionsImperative animate() calls
Scroll-triggered animationsCustom useMotionValue / useSpring
Layout animationsSVG path animations
Drag interactionsGesture recognizers beyond hover/tap/focus
Variants and staggerPhysics-based custom animations

Both approaches coexist in the same application.

Next steps