logo

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

See how variants simplify complex, multi-element animations. Hover over the cards to see the difference in code organization and maintainability.

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

Define your animation states as an object, then reference them by name. Click the buttons to see how the box animates between different variant states.
start
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

Variants excel at managing multiple states. This upload button demonstrates smooth transitions between idle, active, and success states with perfectly coordinated child animations.
1
idle
Click to start
2
active
In progress
3
success
Completed
// 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

Critical Concept: Propagation works by matching the 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 Rotation:
Child Rotation:
Total: 0°

Parent HAS Variants

Both parent and child animate

P: 180°
C: -180°

Hover or click to see both elements animate

P
180°
+
C
-180°
=0°

✨ 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

Watch how the parent's animation state automatically propagates to all children. When you toggle between states, every child element animates to its matching variant - no individual animation props needed!

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

Watch how animation state flows through multiple nesting levels. The parent's state cascades down through containers, sections, and individual items - creating a perfectly orchestrated animation sequence.

Navigation Menu

Main

Dashboard

Overview & analytics

New

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

1️⃣

Parent Animates

Parent component animates to a variant label like "open"

2️⃣

Context Propagates

Motion creates a context that passes the label to all children

3️⃣

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

Variant labels can be functions that return animation objects. This enables dynamic, randomized, or calculated animations. Each bubble gets a random color on mount.
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

The 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, ... } }
0.5s
0.2s
0.5s
0.2s
1s
0.2s
0.5s
0.2s
0.5s

Center dot has longest delay (1s)

Progress Steps with Custom Themes

Each step in this checkout process has its own color theme. The 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

Create a premium feature card with coordinated hover animations. All child elements should animate through variant propagation when the card is hovered, creating a cohesive and polished effect.

Premium Features

Unlock advanced capabilities and boost your productivity with our premium tier

// Define variants for each element
const cardVariants = {
  idle: {
    scale: 1,
    rotate: 0,
    boxShadow: '0 10px 30px rgba(0, 0, 0, 0.2)',
    background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
  },
  hover: {
    scale: 1.05,
    rotate: 1,
    boxShadow: '0 30px 60px rgba(0, 0, 0, 0.3)',
    background: 'linear-gradient(135deg, #764ba2 0%, #f093fb 100%)',
    transition: {
      type: 'spring' as const,
      stiffness: 300,
      damping: 20,
    },
  },
};

const iconContainerVariants = {
  idle: {
    scale: 1,
    rotate: 0,
    backgroundColor: 'rgba(255, 255, 255, 0.1)',
  },
  hover: {
    scale: 1.1,
    rotate: 360,
    backgroundColor: 'rgba(255, 255, 255, 0.2)',
    transition: {
      rotate: { duration: 0.6, ease: 'easeInOut' },
      scale: { type: 'spring', stiffness: 400 },
    },
  },
};

const titleVariants = {
  idle: { x: 0, opacity: 1 },
  hover: { x: 10, opacity: 1 },
};

const buttonVariants = {
  idle: { scale: 1, x: 0 },
  hover: { scale: 1.05, x: 5 },
};

// Parent component with automatic propagation
<motion.div
  className='card'
  variants={cardVariants}
  initial='idle'
  whileHover='hover' // This propagates to all children!
>
  <motion.div variants={iconContainerVariants}>
    <motion.div variants={iconVariants}>
      <Icon />
    </motion.div>
  </motion.div>
  
  <motion.h3 variants={titleVariants}>
    Premium Features
  </motion.h3>
  
  <motion.button variants={buttonVariants}>
    Get Started
  </motion.button>
</motion.div>

🎯 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

Create an e-commerce product card gallery with variant-based state management. Each card has multiple states (idle, hover, selected, loading) with coordinated animations propagating to child elements.
0
🎧

Wireless Headphones

4.5
Price

$99.99

Smart Watch

4.8
Price

$249.99

💻

Laptop Stand

4.2
Price

$49.99

⌨️

Mechanical Keyboard

4.9
Price

$159.99

// Product card variants with multiple states
const cardVariants = {
  idle: {
    scale: 1,
    boxShadow: '0 4px 20px rgba(0, 0, 0, 0.1)',
  },
  hover: {
    scale: 1.03,
    boxShadow: '0 20px 40px rgba(0, 0, 0, 0.15)',
  },
  selected: {
    scale: 1,
    boxShadow: '0 0 0 2px rgba(255, 255, 255, 0.3)',
  },
  loading: {
    scale: 0.98,
    opacity: 0.8,
  },
};

// Child element variants inherit parent state
const imageVariants = {
  idle: { scale: 1, rotate: 0 },
  hover: { scale: 1.1, rotate: 5 },
  selected: { scale: 1.2, rotate: 0 },
  loading: { scale: 0.9, rotate: 0 },
};

const buttonVariants = {
  idle: { backgroundColor: '#8b5cf6' },
  hover: { backgroundColor: '#7c3aed' },
  selected: { backgroundColor: '#22c55e' },
  loading: { backgroundColor: '#6b7280' },
};

// Dynamic state management
const getProductState = (productId) => {
  if (loadingProducts.includes(productId)) return 'loading';
  if (selectedProducts.includes(productId)) return 'selected';
  return 'idle';
};

// Product card with variant propagation
<motion.div
  variants={cardVariants}
  initial='idle'
  animate={getProductState(product.id)}
  whileHover={state === 'idle' ? 'hover' : state}
>
  {/* All children automatically receive parent state */}
  <motion.div variants={imageVariants}>
    🎧
  </motion.div>
  
  <motion.p variants={priceVariants}>
    $99.99
  </motion.p>
  
  <motion.button variants={buttonVariants}>
    Add to Cart
  </motion.button>
</motion.div>

// Cart bump animation using key prop
<motion.div
  key={cartCount} // Re-mount on count change
  initial={{ scale: 1 }}
  animate={{ scale: 1 }}
>
  <motion.span
    key={cartCount} // Animate number on change
    initial={{ scale: 0 }}
    animate={{ scale: 1 }}
    transition={{ 
      type: 'spring' as const, 
      stiffness: 500 
    }}
  >
    {cartCount}
  </motion.span>
</motion.div>

🎯 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

Create a Spotify-style music player with variant-based animations. The player has multiple states (playing, paused, loading) with all child elements coordinating through parent variants.
🎵

Awesome Song Title

Amazing Artist

1:233:45
// Player state variants
const playerVariants = {
  playing: {
    backgroundColor: '#1a1a1a',
    boxShadow: '0 0 40px rgba(139, 92, 246, 0.3)',
  },
  paused: {
    backgroundColor: '#0f0f0f',
    boxShadow: '0 0 20px rgba(0, 0, 0, 0.5)',
  },
  loading: {
    backgroundColor: '#0f0f0f',
    boxShadow: '0 0 20px rgba(0, 0, 0, 0.5)',
  },
};

// Rotating album art when playing
const albumArtVariants = {
  playing: {
    rotate: 360,
    scale: 1,
    transition: {
      rotate: {
        duration: 20,
        repeat: Infinity,
        ease: 'linear' as const,
      },
    },
  },
  paused: {
    rotate: 0,
    scale: 0.95,
  },
  loading: {
    rotate: 0,
    scale: 0.9,
    opacity: 0.5,
  },
};

// Animated equalizer bars
const equalizerVariants = {
  playing: (custom: number) => ({
    height: '100%',
    transition: {
      duration: 0.5,
      repeat: Infinity,
      repeatType: 'reverse' as const,
      delay: custom * 0.1, // Stagger bars
      ease: 'easeInOut' as const,
    },
  }),
  paused: {
    height: '20%',
  },
  loading: {
    height: '50%',
    opacity: 0.5,
  },
};

// Main player component
<motion.div
  variants={playerVariants}
  initial='paused'
  animate={playerState} // 'playing' | 'paused' | 'loading'
>
  {/* Album art inherits state */}
  <motion.div variants={albumArtVariants}>
    🎵
  </motion.div>
  
  {/* Equalizer bars with custom delays */}
  {[0, 1, 2, 3, 4].map((i) => (
    <motion.div
      key={i}
      custom={i}
      variants={equalizerVariants}
    />
  ))}
  
  {/* Play button with state */}
  <motion.button
    variants={playButtonVariants}
    whileHover={{ scale: 1.05 }}
    whileTap={{ scale: 0.95 }}
  />
</motion.div>

🎯 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