Variants: Named Animation States
Variants are one of Motion's most powerful features, allowing you to define reusable animation states and create coordinated animations with ease.
Why Use Variants?
As your animations become more complex, defining animation properties inline can quickly become messy and hard to maintain. Variants solve this by letting you define named animation states that can be reused and organized.
Inline Animations vs Variants
Inline Animation Objects
Gets messy with multiple animated elements
Premium Features
Unlock advanced capabilities and boost your productivity
❌ Every element needs repetitive animation definitions
// ❌ Inline approach - repetitive and hard to maintain
<motion.div
animate={{
scale: isHovered ? 1.05 : 1,
rotate: isHovered ? 2 : 0,
boxShadow: isHovered
? '0 20px 40px rgba(0, 0, 0, 0.2)'
: '0 4px 20px rgba(0, 0, 0, 0.1)',
}}
onHoverStart={() => setIsHovered(true)}
onHoverEnd={() => setIsHovered(false)}
>
<motion.div
animate={{
scale: isHovered ? 1.2 : 1,
rotate: isHovered ? 180 : 0,
y: isHovered ? -5 : 0,
}}
>
<Icon />
</motion.div>
<motion.h3
animate={{
x: isHovered ? 5 : 0,
color: isHovered ? '#fbbf24' : '#e5e7eb',
}}
>
Title
</motion.h3>
<motion.p
animate={{
opacity: isHovered ? 1 : 0.7,
y: isHovered ? -2 : 0,
}}
>
Description
</motion.p>
<motion.div
animate={{
scale: isHovered ? 1 : 0,
opacity: isHovered ? 1 : 0,
rotate: isHovered ? 0 : -180,
}}
>
<Badge />
</motion.div>
</motion.div>
// Problems:
// 1. Repeated isHovered checks everywhere
// 2. Animation logic scattered across components
// 3. Hard to maintain consistency
// 4. Difficult to coordinate timing
Benefits of Using Variants
Better Organization
- Group related animations together
- Define states once, use everywhere
- Cleaner component code
- Easier to understand intent
Reusability
- Share animations across components
- Create animation libraries
- Consistent motion patterns
- Less code duplication
Animation Propagation
- Parent variants cascade to children
- Automatic coordination
- No prop drilling needed
- Complex choreography made simple
Dynamic & Flexible
- Pass dynamic values to variants
- Compute animations at runtime
- Respond to user preferences
- Create adaptive animations
When to Use Variants
✅ Use variants when:
- You have multiple animation states (idle, hover, active, etc.)
- You need to coordinate parent-child animations
- You want to reuse animations across components
- You want cleaner, more maintainable code
❌ Keep it simple when:
- You have a single, simple animation
- The animation is unique and won't be reused
- You're prototyping quickly
Basic Variants Usage
At its core, variants are simply objects that map names to animation states. Instead of writing animation properties inline, you define them once and reference them by name.
Basic Variants Usage
const boxVariants = {
start: {
rotate: 0,
scale: 1,
borderRadius: '12px',
},
end: {
rotate: 180,
scale: 1.2,
borderRadius: '60px',
},
};
// Example 1: When currentVariant = 'start'
<motion.div
variants={boxVariants}
animate='start' // Matches boxVariants.start property
transition={{ duration: 0.5, type: 'spring' }}
>
start
</motion.div>
// Example 2: When currentVariant = 'end'
<motion.div
variants={boxVariants}
animate='end' // Matches boxVariants.end property
transition={{ duration: 0.5, type: 'spring' }}
>
end
</motion.div>
Multiple Animation States
// Import Lucide icons
import { Upload, Clock, CheckCircle } from 'lucide-react';
// Define variants for coordinated animations
const containerVariants = {
idle: { scale: 1 },
active: { scale: 0.98 },
success: { scale: 1 },
};
const iconContainerVariants = {
idle: { scale: 1, rotate: 0 },
active: { scale: 1, rotate: 0 },
success: { scale: 1.15, rotate: 0 },
};
const progressBarVariants = {
idle: { scaleX: 0 },
active: { scaleX: 0.6 },
success: { scaleX: 1 },
};
// Main component with state management
<motion.button
className={cn(
'button-base',
currentState === 'idle' && 'bg-blue-500',
currentState === 'active' && 'bg-purple-500',
currentState === 'success' && 'bg-green-500'
)}
variants={containerVariants}
animate={currentState}
onClick={handleClick}
>
{/* Icon container */}
<motion.div variants={iconContainerVariants}>
{/* Upload icon */}
{currentState === 'idle' && <Upload />}
{/* Clock icon with continuous rotation */}
{currentState === 'active' && (
<motion.div
animate={{ rotate: 360 }}
transition={{
duration: 2,
repeat: Infinity,
ease: 'linear'
}}
>
<Clock />
</motion.div>
)}
{/* Success icon */}
{currentState === 'success' && <CheckCircle />}
</motion.div>
{/* Text that changes based on state */}
<div>
{currentState === 'idle' && 'Upload File'}
{currentState === 'active' && 'Processing...'}
{currentState === 'success' && 'Upload Complete!'}
</div>
{/* Progress bar */}
<motion.div
className='progress-bar'
variants={progressBarVariants}
style={{ originX: 0 }} // Important: scales from left, not center
transition={{
duration: currentState === 'active' ? 2 : 0.3,
ease: currentState === 'active' ? 'linear' : 'easeOut'
}}
/>
</motion.button>
💡 How This Works
The button automatically moves through three states: idle → active → success. Each state has its own colors and animations. When you click, all the child elements (icon, text, progress bar) animate together because they share the same state names in their variants.
Key Concepts
1. Variant Definition
const variants = {
labelName: { property: value },
anotherLabel: { property: value }
}
2. Using Variants
<motion.div
variants={variants}
animate="labelName" // String 'labelName' matches variants.labelName
whileHover="anotherLabel" // String 'anotherLabel' matches variants.anotherLabel
/>
• variants: Pass your variants object to the element
• animate: Animate to a specific variant label
• initial: Set the starting variant
• whileHover/whileTap: Temporary states on interaction
Best Practices
1. Keep variants close to usage - Define them in the same file or component
2. Use meaningful names - "expanded" vs "collapsed" instead of "state1" vs "state2"
3. Group related properties - All properties for a state in one object
4. Consider reusability - Extract common variants to shared constants
Animation Propagation
One of the most powerful features of variants is automatic propagation. But here's the critical part: propagation works by matching string labels, not by matching animation properties!
🎯 The Truth About Propagation
string labels
used in parent's animate/whileHover/whileTap props with the property names
in child's variants. The parent doesn't even need variants!Parent HAS Variants
Both parent and child animate
Hover or click to see both elements animate
✨ Perfect alignment - appears in original position!
// Parent WITH variants
const parentVariants = {
start: {
rotate: 180,
borderRadius: '30px',
background: 'linear-gradient(to bottom right, #818cf8, #a855f7)'
},
hoverState: { borderRadius: '75px' },
tapState: {
background: '#f59e0b', // Amber color on tap
borderRadius: '75px'
},
};
const childVariants = {
start: {
rotate: -180, // Perfect cancellation!
background: 'linear-gradient(to bottom right, #fb923c, #f97316)'
},
hoverState: { borderRadius: '30px' },
tapState: {
background: '#dc2626', // Red color on tap
borderRadius: '30px'
},
};
<motion.div
variants={parentVariants}
animate='start' // String 'start' triggers child's 'start' variant
whileHover='hoverState' // String 'hoverState' triggers child's 'hoverState' variant
whileTap='tapState' // String 'tapState' triggers child's 'tapState' variant
>
<motion.div variants={childVariants} />
</motion.div>
Key Insight #1: Label Matching
It's the string label (like 'start', 'hoverState') that triggers propagation, NOT the animation properties. The parent passes these strings down, and any child with matching variant property names will animate!
Key Insight #2: Transform Composition
When a parent rotates, it rotates its entire coordinate system - including all children! Child animations are additive to parent transforms:
- Parent rotates its entire coordinate system (including children)
- Child rotation is applied within the parent's rotated space
- Visual result = Parent rotation + Child rotation
- Negative child rotations can cancel parent rotations
- Try Parent: 180° + Child: -180° = 0° (perfect cancellation!)
The child IS animating! It's just that its animation might cancel out the parent's transform.
⚠️ Common Misconception
❌ WRONG: "Properties must match"
Many think propagation happens because parent and child have the same properties:
// This is NOT why propagation works!
parentVariants = {
open: { scale: 1.1 } // scale property
}
childVariants = {
open: { scale: 0.9 } // also scale property
}
✅ RIGHT: "Label names must match"
Propagation works because the string label matches:
// This IS why propagation works!
<motion.div animate='open'> // 'open' string
<motion.div variants={{
open: { x: 100 } // 'open' matches!
}} />
</motion.div>
Remember: The parent's animate/whileHover/whileTap string values must match the child's variant property names. The actual animation properties can be completely different!
Basic Animation Propagation
Premium Features
Click to expand and see propagation
Auto-propagation
Child animations follow parent state automatically
No prop drilling
Clean code without passing animation props
Perfect sync
All elements animate in perfect coordination
// Define variants for each element
const parentVariants = {
collapsed: {
backgroundColor: '#1e293b', // slate-800
borderColor: 'rgba(148, 163, 184, 0.2)',
},
expanded: {
backgroundColor: '#312e81', // indigo-900
borderColor: 'rgba(129, 140, 248, 0.5)',
},
};
const iconVariants = {
collapsed: { rotate: 0, scale: 1 },
expanded: { rotate: 180, scale: 1.1 },
};
const contentVariants = {
collapsed: { opacity: 0, height: 0, y: -10 },
expanded: { opacity: 1, height: 'auto', y: 0 },
};
const featureVariants = {
collapsed: { opacity: 0, x: -20 },
expanded: { opacity: 1, x: 0 },
};
// Build the UI - parent controls everything!
<motion.div
className='w-400 rounded-2xl border-2 p-xl'
variants={parentVariants}
initial='collapsed'
animate={currentState} // This state propagates to ALL children
onClick={() => setCurrentState(currentState === 'collapsed' ? 'expanded' : 'collapsed')}
>
<div className='flex items-center justify-between'>
<motion.h3 variants={titleVariants}>
Premium Features
</motion.h3>
<motion.div variants={iconVariants}>
<ChevronDown className='h-24 w-24' />
</motion.div>
</div>
{/* Content animates based on parent state */}
<motion.div variants={contentVariants}>
<motion.div
variants={featureVariants}
transition={{ delay: 0.1 }} // Stagger effect
>
<Sparkles /> Auto-propagation
</motion.div>
<motion.div
variants={featureVariants}
transition={{ delay: 0.2 }}
>
<Zap /> No prop drilling
</motion.div>
<motion.div
variants={featureVariants}
transition={{ delay: 0.3 }}
>
<Heart /> Perfect sync
</motion.div>
</motion.div>
</motion.div>
Multi-Level Propagation
Navigation Menu
Main
Dashboard
Overview & analytics
Analytics
Detailed reports
Settings
Team
Manage members
Preferences
App settings
Click the menu to open and watch the cascade effect
// Define ALL variants for multi-level propagation
const containerVariants = {
closed: {
backgroundColor: '#1e293b', // slate-800
borderColor: 'rgba(148, 163, 184, 0.2)',
},
open: {
backgroundColor: '#1e1b4b', // indigo-950
borderColor: 'rgba(129, 140, 248, 0.5)',
}
};
const headerVariants = {
closed: { backgroundColor: 'rgba(99, 102, 241, 0.1)' },
open: { backgroundColor: 'rgba(99, 102, 241, 0.2)' }
};
const titleVariants = {
closed: { x: 0, color: '#e2e8f0' },
open: { x: 10, color: '#c7d2fe' }
};
const iconVariants = {
closed: { rotate: 0, scale: 1 },
open: { rotate: 90, scale: 1.1 }
};
const contentVariants = {
closed: { opacity: 0, height: 0 },
open: { opacity: 1, height: 'auto' }
};
const sectionVariants = {
closed: { opacity: 0, y: -10 },
open: { opacity: 1, y: 0 }
};
const itemVariants = {
closed: { x: -20, opacity: 0, scale: 0.95 },
open: { x: 0, opacity: 1, scale: 1 }
};
const badgeVariants = {
closed: { scale: 0, opacity: 0 },
open: { scale: 1, opacity: 1 }
};
// Build the complete UI - one animate prop controls ALL levels!
<motion.div
className='w-420 rounded-2xl border-2'
variants={containerVariants}
initial='closed'
animate={isOpen ? 'open' : 'closed'} // This propagates everywhere!
onClick={() => setIsOpen(!isOpen)}
>
{/* Level 1: Header with background animation */}
<motion.div className='rounded-t-2xl p-lg' variants={headerVariants}>
<div className='flex items-center justify-between'>
{/* Title slides and changes color */}
<motion.h3 variants={titleVariants}>
Navigation Menu
</motion.h3>
{/* Icon rotates 90 degrees */}
<motion.div variants={iconVariants}>
<Menu className='h-24 w-24' />
</motion.div>
</div>
</motion.div>
{/* Level 2: Content expands/collapses */}
<motion.div variants={contentVariants}>
<div className='p-lg pt-0'>
{/* Level 3: Main section fades in */}
<motion.div variants={sectionVariants} transition={{ delay: 0.1 }}>
<h4>Main</h4>
{/* Level 4: Items slide in from left */}
<motion.div
variants={itemVariants}
transition={{ delay: 0.15 }}
>
<Home /> Dashboard
{/* Badge scales in last */}
<motion.div variants={badgeVariants} transition={{ delay: 0.3 }}>
New
</motion.div>
</motion.div>
<motion.div
variants={itemVariants}
transition={{ delay: 0.2 }}
>
<BarChart3 /> Analytics
</motion.div>
</motion.div>
{/* Another Level 3: Settings section */}
<motion.div variants={sectionVariants} transition={{ delay: 0.25 }}>
<h4>Settings</h4>
<motion.div
variants={itemVariants}
transition={{ delay: 0.3 }}
>
<Users /> Team
</motion.div>
<motion.div
variants={itemVariants}
transition={{ delay: 0.35 }}
>
<Settings /> Preferences
{/* Another badge with even later delay */}
<motion.div variants={badgeVariants} transition={{ delay: 0.45 }}>
•
</motion.div>
</motion.div>
</motion.div>
</div>
</motion.div>
</motion.div>
How Deep Propagation Works
The parent's animate="open"
cascades through 4 levels: Container → Header → Sections → Items. Each level only needs its own variants - Motion handles the propagation automatically. Notice how delays create a beautiful waterfall effect!
🔄 How Propagation Works
Parent Animates
Parent component animates to a variant label like "open"
Context Propagates
Motion creates a context that passes the label to all children
Children Respond
Children with "open" variant automatically animate to that state
Important: Only children with variants
prop and matching labels will animate. Children without variants or with different labels are unaffected.
Propagation Rules - The Complete Guide
THE GOLDEN RULE: String labels in parent's props must match property names in child's variants
animate='foo'
→ triggers child's variants.foo
whileHover='bar'
→ triggers child's variants.bar
whileTap='baz'
→ triggers child's variants.baz
Parent variants are optional - Propagation works without them!
Direct motion children only - Only motion components receive propagation
Works through nesting - Propagates through multiple levels
Individual transitions - Each child can have its own transition
Override possible - Children can override with their own animate prop
Dynamic Variants
Static variants are powerful, but sometimes you need animations that change based on data, user input, or random values. Dynamic variants let you use functions instead of objects to create flexible, data-driven animations.
Dynamic Variants with Functions
const bubbleVariants = {
initial: () => ({
scale: 0,
opacity: 0,
backgroundColor: `hsl(${Math.floor(Math.random() * 360)}, 80%, 50%)`,
}),
animate: {
scale: 1,
opacity: 1,
},
};
<motion.div
initial='initial' // String 'initial' triggers function for each child
animate='animate' // String 'animate' propagates to all children
>
{positions.map((pos, index) => (
<motion.div
key={index}
variants={bubbleVariants}
// Child receives 'initial' string → calls bubbleVariants.initial()
// Child receives 'animate' string → uses bubbleVariants.animate
/>
))}
</motion.div>
🎲 Dynamic Variant Concepts
Function Syntax
const variants = {
// Static variant
static: { x: 100 },
// Dynamic variant (function)
dynamic: () => ({
x: Math.random() * 100
}),
// With parameter
withParam: (custom) => ({
x: custom * 100
})
}
Common Use Cases
- Random values (colors, positions)
- Calculated layouts (grids, circles)
- Data-driven animations
- Responsive animations
- Physics simulations
- Procedural animations
Important: Dynamic variants are called once when the component mounts or when the animation state changes. They don't update continuously.
Dynamic Variants Best Practices
• Keep functions pure - No side effects, just return animation objects
• Use custom prop - Pass data through custom for parameterized animations
• Memoize if needed - For expensive calculations, consider memoization
• Test edge cases - Ensure functions handle all possible inputs
The Custom Property
The custom
prop is the bridge between your data and dynamic variants. It allows you to pass any value to variant functions, enabling data-driven, parameterized animations.
Custom Delays Pattern
custom
prop passes data to dynamic variants. Here, each dot receives a custom delay value to create a specific animation pattern.const dotVariants = {
initial: { scale: 0, opacity: 0 },
animate: (delay: number) => ({
scale: 1,
opacity: 1,
transition: {
delay: delay, // Use custom delay value
type: 'spring',
stiffness: 200,
},
}),
};
const delays = [0.5, 0.2, 0.5, 0.2, 1, 0.2, 0.5, 0.2, 0.5];
{positions.map((pos, index) => (
<motion.div
key={index}
custom={delays[index]} // Pass delay to variant function
variants={dotVariants}
initial='initial' // String 'initial' → uses dotVariants.initial
animate='animate' // String 'animate' → calls dotVariants.animate(delays[index])
/>
))}
// For example, when index = 4:
// custom={1} (center dot with 1s delay)
// animate='animate' calls dotVariants.animate(1)
// Returns: { scale: 1, opacity: 1, transition: { delay: 1, ... } }
Center dot has longest delay (1s)
Progress Steps with Custom Themes
custom
prop passes color data to create visually distinct progress indicators.Cart
Review items
Details
Enter information
Payment
Secure checkout
Complete
Order confirmed
const steps = [
{
label: 'Cart',
icon: ShoppingCart,
bgColor: '#3b82f6', // blue-500
},
{
label: 'Details',
icon: User,
bgColor: '#8b5cf6', // purple-500
},
{
label: 'Payment',
icon: CreditCard,
bgColor: '#f97316', // orange-500
},
{
label: 'Complete',
icon: CheckCircle,
bgColor: '#10b981', // green-500
},
];
// Icon variants use custom color data
const iconVariants = {
inactive: {
scale: 0.8,
opacity: 0.5,
backgroundColor: '#404040', // neutral-700
},
active: (step: typeof steps[number]) => ({
scale: 1,
opacity: 1,
backgroundColor: step.bgColor, // Custom color for each step
transition: {
duration: 0.3,
type: 'spring',
},
}),
complete: (step: typeof steps[number]) => ({
scale: 1,
opacity: 1,
backgroundColor: step.bgColor, // Maintains custom color
}),
};
// Usage - Icon background animates to custom color
<motion.div
key={step.label}
custom={step} // Pass entire step object
animate={isActive ? 'active' : 'inactive'}
>
<motion.div
className='flex h-60 w-60 items-center justify-center rounded-full'
style={{ backgroundColor: '#404040' }} // Initial color
variants={iconVariants}
custom={step} // Pass step data to variant function
>
<Icon className='h-24 w-24 text-white' />
</motion.div>
</motion.div>
💡 Real-world usage: Using custom color schemes for different process steps helps users visually distinguish between stages. This is common in multi-step forms, wizards, and checkout flows.
📦 Custom Property Guide
How It Works
// 1. Define dynamic variant
const variants = {
animate: (custom) => ({
x: custom * 100
})
};
// 2. Pass custom value
<motion.div
custom={5}
variants={variants}
animate='animate'
/>
Common Use Cases
- Timing & Delays (step durations)
- Position & Order (array indices)
- Distance Calculations (proximity effects)
- State-based Values (loading times)
- Layout Information (grid positions)
- User Preferences (animation speed)
Propagation: The custom prop also propagates to children, just like variant labels. Children can use the parent's custom value or override with their own.
Custom Property Best Practices
• Type your custom data - Use TypeScript interfaces for complex objects
• Keep it serializable - Avoid passing functions or DOM elements
• Document usage - Make it clear what custom values are expected
• Consider performance - Complex calculations should be memoized
Practice: Variants
Let's practice what you've learned about variants. These exercises will help you master named animation states, propagation, and dynamic animations through real-world examples.
Practice 1: Premium Animated Card
Premium Features
Unlock advanced capabilities and boost your productivity with our premium tier
🎯 Challenge Requirements:
- Create a premium feature card with gradient background
- Card should scale and rotate slightly on hover
- Icon should rotate 360° in container that rotates -360°
- Title slides to the right with spring animation
- Description moves up and increases opacity
- Button scales and slides with arrow animation
- Floating badge appears with spring effect
- Sparkle decorations animate infinitely on hover
- All animations coordinate through parent hover state
Practice 2: Interactive Product Card Gallery
Wireless Headphones
$99.99
Smart Watch
$249.99
Laptop Stand
$49.99
Mechanical Keyboard
$159.99
🎯 Challenge Requirements:
- Create product cards with 4 states: idle, hover, selected, loading
- Use variant propagation to coordinate child element animations
- Implement loading state when adding to cart
- Show selected state with green checkmark after adding
- Animate cart counter with bump effect when items are added
- Different animations for image, price, and button based on state
- Disable hover effects during loading/selected states
Practice 3: Music Player Controls
Awesome Song Title
Amazing Artist
🎯 Challenge Requirements:
- Create player with 3 states: playing, paused, loading
- Album artwork rotates continuously when playing
- Equalizer bars animate with staggered delays when playing
- Play/pause button morphs between states
- Progress bar changes color based on state
- Container background and shadow change with state
- All animations coordinate through parent variants
- Volume slider with hover effects