Documentation
Keyframe Animations
Looping animations, repeat control, multi-step keyframes, repeat types, pulse effects, and loading spinners with motionwind classes -- plus how to reach for Motion's API when you need full choreography.
Keyframe Animations
motionwind supports repeating animations, infinite loops, multi-step keyframe arrays, repeat types (reverse/mirror), timeline control with times, and duration/easing control -- all through its class syntax. For very complex choreographed sequences, you can still drop down to the Motion API directly. This page covers both approaches.
Continuous spin (infinite loop)
Combine animate-enter:rotate-360 with animate-repeat-infinite, a duration, and linear easing to create a continuous rotation that never stops.
<div className="animate-enter:rotate-360 animate-repeat-infinite animate-duration-2000 animate-ease-linear">
MW
</div>At build time, motionwind compiles this to:
<motion.div
animate={{ rotate: 360 }}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear",
}}
>
MW
</motion.div>What each class does
| Class | Motion equivalent | Purpose |
|---|---|---|
animate-enter:rotate-360 | animate={{ rotate: 360 }} | Target rotation of 360 degrees |
animate-repeat-infinite | transition.repeat = Infinity | Loop forever |
animate-duration-2000 | transition.duration = 2 | 2 seconds per revolution |
animate-ease-linear | transition.ease = "linear" | Constant speed, no acceleration |
Repeat a fixed number of times
Use animate-repeat-{n} to repeat an animation a specific number of times instead of infinitely.
<div className="animate-enter:rotate-360 animate-repeat-3 animate-duration-800 animate-ease-in-out">
3x
</div>This compiles to:
<motion.div
animate={{ rotate: 360 }}
transition={{
duration: 0.8,
repeat: 3,
ease: "easeInOut",
}}
>
3x
</motion.div>The element rotates 360 degrees three times, then stops.
Pulse effect
A pulse animation scales an element up and down continuously. Use animate-initial: to set the starting scale and animate-enter: for the target, then loop it infinitely.
<div className="animate-initial:scale-100 animate-enter:scale-120 animate-repeat-infinite animate-repeat-reverse animate-duration-1000 animate-ease-in-out">
<!-- inner content -->
</div>Compiled output:
<motion.div
initial={{ scale: 1 }}
animate={{ scale: 1.2 }}
transition={{
duration: 1,
repeat: Infinity,
repeatType: "reverse",
ease: "easeInOut",
}}
>
{/* inner content */}
</motion.div>Motion automatically alternates between initial and animate values when repeat is set, which creates the pulsing back-and-forth effect.
Loading spinner
A loading spinner is a continuous rotation with linear easing so the speed stays constant.
<div className="animate-enter:rotate-360 animate-repeat-infinite animate-duration-1000 animate-ease-linear w-10 h-10 rounded-full border-4 border-[#c8ff2e]/20 border-t-[#c8ff2e]" />The Tailwind border utilities create the visual spinner shape. The motionwind classes handle the rotation. At build time, the animate-* classes are stripped and the element is rewritten as a motion.div with the correct props. The Tailwind classes (w-10, h-10, rounded-full, border-4, etc.) pass through untouched.
Multi-step keyframes
motionwind supports multi-step keyframe arrays directly in the class syntax. Pass a comma-separated list of values inside square brackets to animate through multiple steps.
Syntax
animate-{gesture}:{property}-[v1,v2,v3,...]Each value in the bracket becomes an entry in Motion's keyframe array. Scale and opacity values are divided by 100 automatically (just like single-value classes), so scale-[100,120,100] becomes scale: [1, 1.2, 1].
Supported properties
Keyframe arrays work with all animatable properties: scale, scale-x, scale-y, scale-z, rotate, rotate-x, rotate-y, x, y, z, opacity, skew-x, skew-y, w, h, rounded, origin-x, origin-y, origin-z, perspective.
Breathing animation
A simple breathing effect that scales up and back down continuously:
<div className="animate-enter:scale-[100,120,100] animate-repeat-infinite animate-duration-2000 animate-ease-in-out">
<!-- inner content -->
</div>Compiled output:
<motion.div
animate={{ scale: [1, 1.2, 1] }}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
}}
>
{/* inner content */}
</motion.div>The three values [100,120,100] become [1, 1.2, 1] after the automatic division by 100. Motion interpolates between them evenly across the duration, with easeInOut making each phase feel natural.
Multi-step translate (rectangular path)
Combine keyframe arrays on multiple properties to trace complex paths:
<div className="animate-enter:x-[0,100,100,0] animate-enter:y-[0,0,-50,0] animate-duration-3000 animate-repeat-infinite animate-ease-in-out">
</div>Compiled output:
<motion.div
animate={{
x: [0, 100, 100, 0],
y: [0, 0, -50, 0],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut",
}}
/>The element traces a rectangular path: right, up, left/down, back to start -- all defined purely with classes.
Combining keyframes with motionwind
You can mix direct Motion props and motionwind classes on sibling elements. For example, a parent with Motion keyframes and a child with motionwind hover effects:
import { motion } from "motion/react";
<motion.div
animate={{ x: [0, 100, 0] }}
transition={{ duration: 2, repeat: Infinity }}
className="p-4"
>
{/* This child uses motionwind classes -- Babel transforms it at build time */}
<div className="animate-hover:scale-110 animate-tap:scale-90 animate-spring">
Hover me while I move
</div>
</motion.div>The Babel plugin transforms the inner div into a motion.div with hover and tap props. The outer motion.div uses Motion's keyframes API directly. Both work together without conflict.
Reverse and alternate animations
motionwind provides classes to control Motion's repeatType property, which determines what happens after each repetition cycle.
Available classes
| Class | Motion equivalent | Behavior |
|---|---|---|
animate-repeat-reverse | transition.repeatType = "reverse" | Alternates direction each cycle (forward, backward, forward, ...) |
animate-repeat-mirror | transition.repeatType = "mirror" | Same behavior as reverse -- mirror naming alias |
Ping-pong slide
Use animate-repeat-reverse to make an element slide back and forth:
<div className="animate-enter:x-100 animate-repeat-infinite animate-repeat-reverse animate-duration-1000">
</div>Compiled output:
<motion.div
animate={{ x: 100 }}
transition={{
duration: 1,
repeat: Infinity,
repeatType: "reverse",
}}
/>Without animate-repeat-reverse, a repeating animation would snap back to the start position and play forward again (the "loop" behavior). With it, the animation smoothly reverses direction on each cycle.
Repeat delay
Use animate-repeat-delay-{ms} to add a pause between each repetition cycle. The value is in milliseconds and is converted to seconds for Motion.
| Class | Motion equivalent | Purpose |
|---|---|---|
animate-repeat-delay-500 | transition.repeatDelay = 0.5 | 500ms pause between cycles |
animate-repeat-delay-1000 | transition.repeatDelay = 1 | 1 second pause between cycles |
Pulse with pause
A pulse animation that rests briefly between each beat:
<div className="animate-initial:scale-100 animate-enter:scale-120 animate-repeat-infinite animate-repeat-reverse animate-repeat-delay-500 animate-duration-600 animate-ease-in-out">
<!-- inner content -->
</div>Compiled output:
<motion.div
initial={{ scale: 1 }}
animate={{ scale: 1.2 }}
transition={{
duration: 0.6,
repeat: Infinity,
repeatType: "reverse",
repeatDelay: 0.5,
ease: "easeInOut",
}}
>
{/* inner content */}
</motion.div>Complex timelines with times
motionwind supports the times array through the animate-times-[...] class. The times array controls exactly when each keyframe is reached during the total duration, giving you precise timing control over multi-step keyframe animations.
Syntax
animate-times-[v1,v2,v3,...]Each value maps directly to Motion's times array. Values should range from 0 to 1, representing the fraction of total duration at which each keyframe is reached.
Square path with precise timing
Combine keyframe arrays with times to control exactly when each step of the animation occurs:
<div className="animate-enter:x-[0,100,100,0,0] animate-enter:y-[0,0,-100,-100,0] animate-times-[0,0.25,0.5,0.75,1] animate-duration-4000 animate-repeat-infinite animate-ease-linear">
</div>Compiled output:
<motion.div
animate={{
x: [0, 100, 100, 0, 0],
y: [0, 0, -100, -100, 0],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "linear",
times: [0, 0.25, 0.5, 0.75, 1],
}}
/>The element traces a square path, spending exactly 25% of the total duration on each side. Without times, Motion distributes keyframes evenly -- with times, you have full control over the pacing.
When to reach for the Motion API
For very complex choreography -- such as staggered children with independent per-property timing, or orchestrated sequences across multiple elements -- Motion's full orchestration API is still available and may be more readable than long class strings. You can always mix direct Motion props alongside motionwind classes on sibling elements.
When to use which approach
| Need | Approach |
|---|---|
| Simple loop (spin, pulse, bounce) | motionwind classes |
| Fixed repeat count | animate-repeat-{n} class |
| Duration and easing | animate-duration-{ms} and animate-ease-* classes |
| Multi-step keyframes | animate-enter:{prop}-[v1,v2,v3] class |
| Reverse / mirror repeat | animate-repeat-reverse or animate-repeat-mirror class |
| Pause between cycles | animate-repeat-delay-{ms} class |
| Precise keyframe timing | animate-times-[...] class |
| Staggered children / orchestration across elements | Motion orchestration API |
| Independent per-property transition config | Motion transition prop directly |