Keyframe animations let you define multiple values for a property, creating complex animation sequences that move through each value in order.
Basic Keyframes
Pass an array of values to animate through:
<motion.div
animate={{
x: [0, 100, 0], // Move right, then back
rotate: [0, 180, 360] // Full rotation
}}
transition={{ duration: 2 }}
/>
The element will animate through each value sequentially over the duration.
Timing Control
Control when each keyframe is reached using the times array:
<motion.div
animate={{
x: [0, 100, 100, 0],
opacity: [0, 1, 1, 0]
}}
transition={{
duration: 2,
times: [0, 0.2, 0.8, 1] // Reach values at these progress points
}}
/>
times values must be between 0 and 1
- First value must be 0, last must be 1
- Array length must match keyframe array length
If you don’t specify times, keyframes are evenly distributed across the duration.
Per-Keyframe Easing
Apply different easing functions between each keyframe:
<motion.div
animate={{
x: [0, 100, 0]
}}
transition={{
duration: 2,
ease: ["easeOut", "easeIn"] // One less than keyframe count
}}
/>
From keyframes.ts:34-36, easing functions are converted and applied:
const easingFunctions = isEasingArray(ease)
? ease.map(easingDefinitionToFunction)
: easingDefinitionToFunction(ease)
Real-World Examples
Loading Pulse
Create a subtle pulsing animation:
<motion.div
animate={{
scale: [1, 1.05, 1],
opacity: [0.7, 1, 0.7]
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
>
Loading...
</motion.div>
Attention Seeker
Shake animation to draw attention:
<motion.div
animate={{
x: [0, -10, 10, -10, 10, 0],
rotate: [0, -5, 5, -5, 5, 0]
}}
transition={{
duration: 0.5,
times: [0, 0.2, 0.4, 0.6, 0.8, 1]
}}
>
New notification!
</motion.div>
Color Cycle
Cycle through multiple colors:
<motion.div
animate={{
backgroundColor: [
"#ff0080",
"#7928ca",
"#0070f3",
"#00ff00",
"#ff0080"
]
}}
transition={{
duration: 5,
repeat: Infinity,
ease: "linear"
}}
/>
Bouncing Ball
Realistic bounce with varying timing:
<motion.div
animate={{
y: [0, -150, 0, -100, 0, -50, 0],
scaleY: [1, 1.1, 0.9, 1.05, 0.95, 1.02, 1]
}}
transition={{
duration: 2,
times: [0, 0.2, 0.4, 0.55, 0.7, 0.85, 1],
ease: [
[0.42, 0, 1, 1], // easeIn for up
[0, 0, 0.58, 1], // easeOut for down
[0.42, 0, 1, 1],
[0, 0, 0.58, 1],
[0.42, 0, 1, 1],
[0, 0, 0.58, 1]
]
}}
style={{
width: 50,
height: 50,
borderRadius: '50%',
background: '#ff0080'
}}
/>
Path Animation
Animate along a custom path:
<motion.div
animate={{
x: [0, 100, 200, 200, 100, 0],
y: [0, -50, 0, 100, 150, 100]
}}
transition={{
duration: 3,
times: [0, 0.2, 0.4, 0.6, 0.8, 1],
ease: "easeInOut",
repeat: Infinity
}}
/>
Combining with Variants
Keyframes work seamlessly with variants:
const variants = {
wiggle: {
rotate: [0, 5, -5, 5, -5, 0],
transition: {
duration: 0.5,
times: [0, 0.2, 0.4, 0.6, 0.8, 1]
}
},
pulse: {
scale: [1, 1.1, 1, 1.1, 1],
transition: {
duration: 0.6,
ease: "easeInOut"
}
}
}
<motion.div
variants={variants}
animate="wiggle"
whileHover="pulse"
/>
SVG Path Keyframes
Animate SVG paths through different shapes:
<motion.svg width="100" height="100">
<motion.path
animate={{
d: [
"M 50 50 L 100 50 L 100 100 Z", // Triangle
"M 50 25 L 75 50 L 50 75 L 25 50 Z", // Diamond
"M 50 50 m -25 0 a 25 25 0 1 0 50 0 a 25 25 0 1 0 -50 0" // Circle
]
}}
transition={{ duration: 3, repeat: Infinity }}
fill="#0070f3"
/>
</motion.svg>
Complex Sequences
Orchestrate multiple properties with precise timing:
<motion.div
animate={{
x: [0, 100, 100, 0, 0],
y: [0, 0, 100, 100, 0],
rotate: [0, 0, 90, 180, 360],
borderRadius: ["20%", "20%", "50%", "20%", "20%"],
backgroundColor: [
"#ff0080",
"#7928ca",
"#0070f3",
"#00ff00",
"#ff0080"
]
}}
transition={{
duration: 4,
times: [0, 0.25, 0.5, 0.75, 1],
ease: "easeInOut"
}}
/>
Keyframe Implementation
Motion’s keyframe engine uses interpolation. From keyframes.ts:59-63:
const mapTimeToKeyframe = interpolate<T>(absoluteTimes, keyframeValues, {
ease: Array.isArray(easingFunctions)
? easingFunctions
: defaultEasing(keyframeValues, easingFunctions),
})
The animation state is updated each frame:
next: (t: number) => {
state.value = mapTimeToKeyframe(t)
state.done = t >= duration
return state
}
Repeating Animations
Create infinite loops or limited repeats:
// Infinite loop
<motion.div
animate={{ rotate: [0, 360] }}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
/>
// Repeat 3 times
<motion.div
animate={{ x: [0, 100, 0] }}
transition={{
duration: 1,
repeat: 3,
repeatType: "loop" // "loop", "reverse", or "mirror"
}}
/>
// Yoyo effect (reverse on each repeat)
<motion.div
animate={{ scale: [1, 1.5] }}
transition={{
duration: 1,
repeat: Infinity,
repeatType: "reverse"
}}
/>
Easing Options
Available easing functions:
"linear" - Constant speed
"easeIn" - Slow start
"easeOut" - Slow end
"easeInOut" - Slow start and end
"circIn", "circOut", "circInOut" - Circular easing
"backIn", "backOut", "backInOut" - Overshooting curves
[0.42, 0, 0.58, 1] - Custom cubic bezier
Limit keyframe count - More keyframes = more calculations. Use 3-5 keyframes for most animations.
Animate GPU-accelerated properties - Prefer x, y, scale, rotate, opacity over top, left, width, height.
Use linear easing for continuous loops - Prevents micro-pauses at loop points.
Common Patterns
Typewriter Effect
const text = "Hello, World!"
<motion.div
initial={{ width: 0 }}
animate={{ width: "100%" }}
transition={{
duration: 2,
ease: "steps(" + text.length + ")"
}}
style={{ overflow: "hidden", whiteSpace: "nowrap" }}
>
{text}
</motion.div>
Gradient Shift
<motion.div
animate={{
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"]
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "linear"
}}
style={{
background: "linear-gradient(90deg, #ff0080, #7928ca, #0070f3)",
backgroundSize: "200% 200%"
}}
/>
Next Steps
Spring Animations
Physics-based motion
Scroll Animations
Link keyframes to scroll
Layout Transitions
Animate layout changes
Performance
Optimize complex animations