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.

Continuous spin
<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

ClassMotion equivalentPurpose
animate-enter:rotate-360animate={{ rotate: 360 }}Target rotation of 360 degrees
animate-repeat-infinitetransition.repeat = InfinityLoop forever
animate-duration-2000transition.duration = 22 seconds per revolution
animate-ease-lineartransition.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.

Repeat 3 times
<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.

Pulse
<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.

Loading spinner
<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:

Breathing animation
<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:

Rectangular path
<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

ClassMotion equivalentBehavior
animate-repeat-reversetransition.repeatType = "reverse"Alternates direction each cycle (forward, backward, forward, ...)
animate-repeat-mirrortransition.repeatType = "mirror"Same behavior as reverse -- mirror naming alias

Ping-pong slide

Use animate-repeat-reverse to make an element slide back and forth:

Reverse repeat
<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.

ClassMotion equivalentPurpose
animate-repeat-delay-500transition.repeatDelay = 0.5500ms pause between cycles
animate-repeat-delay-1000transition.repeatDelay = 11 second pause between cycles

Pulse with pause

A pulse animation that rests briefly between each beat:

Pulse with repeat delay
<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:

Square path with times
<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

NeedApproach
Simple loop (spin, pulse, bounce)motionwind classes
Fixed repeat countanimate-repeat-{n} class
Duration and easinganimate-duration-{ms} and animate-ease-* classes
Multi-step keyframesanimate-enter:{prop}-[v1,v2,v3] class
Reverse / mirror repeatanimate-repeat-reverse or animate-repeat-mirror class
Pause between cyclesanimate-repeat-delay-{ms} class
Precise keyframe timinganimate-times-[...] class
Staggered children / orchestration across elementsMotion orchestration API
Independent per-property transition configMotion transition prop directly