MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Web Animations with Framer Motion

Create production animations with Framer Motion: gestures, layout animations, and AnimatePresence.

Framer MotionAnimationsReactFrontend

By MinhVo

Introduction

Framer Motion (now simply called Motion) has become the de facto standard for animations in React applications. Built on top of the Web Animations API, it provides a declarative, physics-based animation system that makes complex animations feel natural and effortless. What sets Motion apart from other animation libraries is its deep integration with React's component model, layout animation capabilities that handle shared element transitions automatically, and an AnimatePresence component that elegantly handles mounting and unmounting animations.

The library was originally created by Framer for their website builder tool but quickly gained popularity in the broader React community. Its API is designed around the principle that animations should be declarative — you describe what you want, and Motion figures out how to get there. This means you rarely need to manage animation state manually, write complex timeline code, or worry about interrupted animations.

In this comprehensive guide, we'll explore every aspect of Motion, from basic motion components and gesture animations to advanced layout animations and page transitions. We'll build real-world examples including animated modals, drag-and-drop interfaces, shared layout transitions, and staggered list animations. By the end, you'll have the knowledge to create polished, performant animations that elevate your React applications.

Framer Motion

Understanding Motion: Core Concepts

The motion Component

Motion provides motion versions of every HTML and SVG element. These components accept animation props that control their visual behavior:

import { motion } from 'framer-motion';
 
// Basic motion component
function AnimatedBox() {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      exit={{ opacity: 0, y: -20 }}
      transition={{ duration: 0.3, ease: 'easeOut' }}
    >
      I'm animated!
    </motion.div>
  );
}
 
// SVG animation
function AnimatedIcon() {
  return (
    <motion.svg
      width="50"
      height="50"
      viewBox="0 0 50 50"
      animate={{ rotate: 360 }}
      transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}
    >
      <motion.circle
        cx="25"
        cy="25"
        r="20"
        fill="none"
        stroke="#005fcc"
        strokeWidth="3"
        initial={{ pathLength: 0 }}
        animate={{ pathLength: 1 }}
        transition={{ duration: 1.5, ease: 'easeInOut' }}
      />
    </motion.svg>
  );
}

Animation Props

interface MotionProps {
  // Initial state (before animation)
  initial?: Target | boolean;
  
  // Target state to animate to
  animate?: Target;
  
  // State when component exits (requires AnimatePresence)
  exit?: Target;
  
  // While hovering
  whileHover?: Target;
  
  // While pressing/clicking
  whileTap?: Target;
  
  // While dragging
  whileDrag?: Target;
  
  // While element has focus
  whileFocus?: Target;
  
  // While element is in viewport
  whileInView?: Target;
  
  // Animation transition configuration
  transition?: Transition;
  
  // Drag constraints and configuration
  drag?: boolean | 'x' | 'y';
  dragConstraints?: { top?: number; bottom?: number; left?: number; right?: number } | RefObject;
  dragElastic?: number;
  dragMomentum?: boolean;
  
  // Layout animation (automatic position/size animation)
  layout?: boolean | string;
  layoutId?: string;
  
  // Variants (named animation states)
  variants?: Variants;
  
  // Controls which variant to show
  custom?: any;
}

Transition Types

// Spring physics (default)
<motion.div
  animate={{ x: 100 }}
  transition={{
    type: 'spring',
    stiffness: 300,  // Higher = faster
    damping: 20,     // Higher = less bouncy
    mass: 1,         // Higher = slower, heavier
  }}
/>
 
// Tween (duration-based)
<motion.div
  animate={{ opacity: 1 }}
  transition={{
    type: 'tween',
    duration: 0.5,
    ease: 'easeInOut', // or cubic-bezier array
  }}
/>
 
// Inertia (deceleration-based)
<motion.div
  animate={{ x: 500 }}
  transition={{
    type: 'inertia',
    velocity: 200,
    power: 0.8,
    timeConstant: 300,
  }}
/>
 
// Keyframes (multiple values)
<motion.div
  animate={{ x: [0, 100, 50, 100] }}
  transition={{
    duration: 1,
    times: [0, 0.3, 0.6, 1], // When each keyframe occurs
  }}
/>

Gesture Animations

Hover and Tap

function InteractiveButton() {
  return (
    <motion.button
      whileHover={{ 
        scale: 1.05,
        boxShadow: '0 10px 20px rgba(0,0,0,0.1)',
      }}
      whileTap={{ scale: 0.95 }}
      transition={{ type: 'spring', stiffness: 400, damping: 17 }}
      style={{
        padding: '12px 24px',
        fontSize: '16px',
        borderRadius: '8px',
        border: 'none',
        cursor: 'pointer',
      }}
    >
      Click me
    </motion.button>
  );
}
 
// Magnetic hover effect
function MagneticButton({ children }: { children: React.ReactNode }) {
  const ref = useRef<HTMLButtonElement>(null);
  const [position, setPosition] = useState({ x: 0, y: 0 });
  
  const handleMouse = (e: React.MouseEvent) => {
    if (!ref.current) return;
    const rect = ref.current.getBoundingClientRect();
    const centerX = rect.left + rect.width / 2;
    const centerY = rect.top + rect.height / 2;
    setPosition({
      x: (e.clientX - centerX) * 0.2,
      y: (e.clientY - centerY) * 0.2,
    });
  };
  
  const resetMouse = () => setPosition({ x: 0, y: 0 });
  
  return (
    <motion.button
      ref={ref}
      onMouseMove={handleMouse}
      onMouseLeave={resetMouse}
      animate={{ x: position.x, y: position.y }}
      whileHover={{ scale: 1.1 }}
      transition={{ type: 'spring', stiffness: 300, damping: 20 }}
    >
      {children}
    </motion.button>
  );
}

Drag Interactions

import { motion, useMotionValue, useTransform } from 'framer-motion';
 
function DraggableCard() {
  const x = useMotionValue(0);
  const y = useMotionValue(0);
  
  // Transform drag distance to rotation
  const rotateX = useTransform(y, [-100, 100], [10, -10]);
  const rotateY = useTransform(x, [-100, 100], [-10, 10]);
  
  return (
    <motion.div
      drag
      dragConstraints={{ top: -100, bottom: 100, left: -100, right: 100 }}
      dragElastic={0.1}
      whileDrag={{ scale: 1.1, cursor: 'grabbing' }}
      style={{ x, y, rotateX, rotateY, cursor: 'grab' }}
      className="card"
    >
      Drag me around
    </motion.div>
  );
}
 
// Swipe to dismiss
function SwipeCard({ onDismiss }: { onDismiss: () => void }) {
  const x = useMotionValue(0);
  const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0]);
  
  return (
    <motion.div
      drag="x"
      dragConstraints={{ left: 0, right: 0 }}
      dragElastic={0.5}
      style={{ x, opacity }}
      onDragEnd={(_, info) => {
        if (Math.abs(info.offset.x) > 100) {
          onDismiss();
        }
      }}
      whileDrag={{ scale: 0.95 }}
    >
      <div className="card-content">Swipe to dismiss</div>
    </motion.div>
  );
}

Animation gestures

AnimatePresence: Mount/Unmount Animations

Basic Usage

import { AnimatePresence, motion } from 'framer-motion';
 
function ToggleContent() {
  const [isVisible, setIsVisible] = useState(true);
  
  return (
    <div>
      <button onClick={() => setIsVisible(!isVisible)}>Toggle</button>
      
      <AnimatePresence mode="wait">
        {isVisible && (
          <motion.div
            key="content"
            initial={{ opacity: 0, height: 0 }}
            animate={{ opacity: 1, height: 'auto' }}
            exit={{ opacity: 0, height: 0 }}
            transition={{ duration: 0.3 }}
          >
            Content goes here
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}
function AnimatedModal({ isOpen, onClose, children }: ModalProps) {
  return (
    <AnimatePresence>
      {isOpen && (
        <motion.div
          className="modal-overlay"
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          onClick={onClose}
        >
          <motion.div
            className="modal-content"
            initial={{ opacity: 0, scale: 0.9, y: 20 }}
            animate={{ opacity: 1, scale: 1, y: 0 }}
            exit={{ opacity: 0, scale: 0.9, y: 20 }}
            transition={{ type: 'spring', damping: 25, stiffness: 300 }}
            onClick={(e) => e.stopPropagation()}
            role="dialog"
            aria-modal="true"
          >
            {children}
            <button onClick={onClose}>Close</button>
          </motion.div>
        </motion.div>
      )}
    </AnimatePresence>
  );
}

Animated List with Add/Remove

import { AnimatePresence, motion } from 'framer-motion';
 
interface Item {
  id: string;
  text: string;
}
 
function AnimatedList() {
  const [items, setItems] = useState<Item[]>([]);
  
  const addItem = () => {
    setItems(prev => [
      ...prev,
      { id: Date.now().toString(), text: `Item ${prev.length + 1}` },
    ]);
  };
  
  const removeItem = (id: string) => {
    setItems(prev => prev.filter(item => item.id !== id));
  };
  
  return (
    <div>
      <button onClick={addItem}>Add Item</button>
      
      <AnimatePresence mode="popLayout">
        {items.map((item) => (
          <motion.div
            key={item.id}
            layout
            initial={{ opacity: 0, x: -100 }}
            animate={{ opacity: 1, x: 0 }}
            exit={{ opacity: 0, x: 100, transition: { duration: 0.2 } }}
            transition={{ type: 'spring', stiffness: 500, damping: 30 }}
            style={{
              padding: '12px',
              margin: '8px 0',
              background: '#f0f0f0',
              borderRadius: '8px',
            }}
          >
            <span>{item.text}</span>
            <button onClick={() => removeItem(item.id)}>×</button>
          </motion.div>
        ))}
      </AnimatePresence>
    </div>
  );
}

Layout Animations

Automatic Layout Transitions

// Automatic layout animation - Motion handles position/size changes
function ExpandableCard() {
  const [isExpanded, setIsExpanded] = useState(false);
  
  return (
    <motion.div
      layout
      onClick={() => setIsExpanded(!isExpanded)}
      style={{
        background: 'white',
        borderRadius: '12px',
        padding: '20px',
        cursor: 'pointer',
      }}
    >
      <motion.h3 layout="position">Card Title</motion.h3>
      
      <AnimatePresence>
        {isExpanded && (
          <motion.p
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
          >
            This content is shown when expanded. Click to collapse.
          </motion.p>
        )}
      </AnimatePresence>
    </motion.div>
  );
}

Shared Layout Animations (layoutId)

// Shared layout between components
function ImageGallery() {
  const [selectedId, setSelectedId] = useState<string | null>(null);
  
  const images = [
    { id: '1', src: '/img1.jpg', title: 'Image 1' },
    { id: '2', src: '/img2.jpg', title: 'Image 2' },
    { id: '3', src: '/img3.jpg', title: 'Image 3' },
  ];
  
  return (
    <div>
      <div className="gallery">
        {images.map((image) => (
          <motion.div
            key={image.id}
            layoutId={image.id}
            onClick={() => setSelectedId(image.id)}
            style={{ cursor: 'pointer' }}
          >
            <motion.img src={image.src} layoutId={`img-${image.id}`} />
            <motion.h5 layoutId={`title-${image.id}`}>{image.title}</motion.h5>
          </motion.div>
        ))}
      </div>
      
      <AnimatePresence>
        {selectedId && (
          <motion.div
            className="overlay"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            onClick={() => setSelectedId(null)}
          >
            <motion.div layoutId={selectedId} className="expanded-card">
              <motion.img
                src={images.find(i => i.id === selectedId)?.src}
                layoutId={`img-${selectedId}`}
              />
              <motion.h5 layoutId={`title-${selectedId}`}>
                {images.find(i => i.id === selectedId)?.title}
              </motion.h5>
            </motion.div>
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}

Layout animations

Variants: Named Animation States

// Define reusable animation states
const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1, // Stagger child animations
      delayChildren: 0.2,
    },
  },
  exit: {
    opacity: 0,
    transition: {
      staggerChildren: 0.05,
      staggerDirection: -1, // Reverse stagger on exit
    },
  },
};
 
const itemVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: { opacity: 1, y: 0 },
  exit: { opacity: 0, y: -20 },
};
 
function StaggeredList({ items }: { items: string[] }) {
  return (
    <motion.ul
      variants={containerVariants}
      initial="hidden"
      animate="visible"
      exit="exit"
    >
      {items.map((item, index) => (
        <motion.li key={index} variants={itemVariants}>
          {item}
        </motion.li>
      ))}
    </motion.ul>
  );
}
 
// Directional variants
const slideVariants = {
  enter: (direction: number) => ({
    x: direction > 0 ? 1000 : -1000,
    opacity: 0,
  }),
  center: {
    zIndex: 1,
    x: 0,
    opacity: 1,
  },
  exit: (direction: number) => ({
    zIndex: 0,
    x: direction < 0 ? 1000 : -1000,
    opacity: 0,
  }),
};
 
function Carousel({ items }: { items: string[] }) {
  const [[page, direction], setPage] = useState([0, 0]);
  
  const paginate = (newDirection: number) => {
    setPage([page + newDirection, newDirection]);
  };
  
  return (
    <AnimatePresence initial={false} custom={direction}>
      <motion.div
        key={page}
        custom={direction}
        variants={slideVariants}
        initial="enter"
        animate="center"
        exit="exit"
        transition={{
          x: { type: 'spring', stiffness: 300, damping: 30 },
          opacity: { duration: 0.2 },
        }}
      >
        {items[page]}
      </motion.div>
    </AnimatePresence>
  );
}

Scroll-Triggered Animations

import { motion, useScroll, useTransform, useInView } from 'framer-motion';
 
// Basic scroll-triggered animation
function ScrollReveal({ children }: { children: React.ReactNode }) {
  const ref = useRef(null);
  const isInView = useInView(ref, { once: true, margin: '-100px' });
  
  return (
    <motion.div
      ref={ref}
      initial={{ opacity: 0, y: 50 }}
      animate={isInView ? { opacity: 1, y: 0 } : {}}
      transition={{ duration: 0.6, ease: 'easeOut' }}
    >
      {children}
    </motion.div>
  );
}
 
// Parallax scroll effect
function ParallaxSection({ children }: { children: React.ReactNode }) {
  const { scrollYProgress } = useScroll();
  const y = useTransform(scrollYProgress, [0, 1], ['0%', '50%']);
  
  return (
    <motion.div style={{ y }}>
      {children}
    </motion.div>
  );
}
 
// Progress-linked scroll
function ScrollProgress() {
  const { scrollYProgress } = useScroll();
  const scaleX = useTransform(scrollYProgress, [0, 1], [0, 1]);
  
  return (
    <motion.div
      style={{
        scaleX,
        transformOrigin: 'left',
        background: '#005fcc',
        height: '4px',
        position: 'fixed',
        top: 0,
        left: 0,
        right: 0,
      }}
    />
  );
}

Advanced Patterns

Orchestrated Animations

// Complex orchestration with useAnimation
import { useAnimation, motion } from 'framer-motion';
 
function SuccessAnimation() {
  const controls = useAnimation();
  
  const runAnimation = async () => {
    // Step 1: Fade in
    await controls.start({ opacity: 1, transition: { duration: 0.3 } });
    
    // Step 2: Scale up
    await controls.start({ scale: 1.2, transition: { duration: 0.2 } });
    
    // Step 3: Bounce back
    await controls.start({ scale: 1, transition: { type: 'spring', stiffness: 500 } });
    
    // Step 4: Show checkmark
    await controls.start({ pathLength: 1, transition: { duration: 0.5 } });
  };
  
  return (
    <motion.div animate={controls} initial={{ opacity: 0, scale: 0.8 }}>
      <motion.svg viewBox="0 0 50 50">
        <motion.path
          d="M14 27l7.8 7.8L36 16"
          fill="none"
          stroke="#22c55e"
          strokeWidth="3"
          initial={{ pathLength: 0 }}
          animate={controls}
        />
      </motion.svg>
    </motion.div>
  );
}

Performance Tips

  1. Use layout sparingly — Layout animations trigger reflows; use only when needed
  2. Prefer transform and opacity — These are compositor-only properties
  3. Use will-change hint — Tell the browser which properties will animate
  4. Animate in batches — Use staggerChildren instead of individual delays
  5. Use mode="wait" for AnimatePresence — Prevents layout jumps during transitions

Best Practices for Production

  1. Use variants for reusable animations — Define states once, apply across components
  2. Respect user preferences — Check prefers-reduced-motion and disable animations
  3. Keep animations subtle — Animations should enhance, not distract
  4. Test on low-end devices — Ensure animations perform well on slower hardware
  5. Use layoutId for shared element transitions — Automatic position/size animation
  6. Leverage useInView for scroll animations — Only animate when visible

Cross-Browser Testing Strategy

Modern CSS features often have varying levels of browser support, making a systematic cross-browser testing strategy essential. Before using any CSS feature in production, verify its support status and implement appropriate fallbacks for browsers that haven't yet implemented it.

Progressive Enhancement with @supports

Use the @supports at-rule to provide fallback styles for browsers that don't support specific CSS features:

/* Base styles for all browsers */
.grid-container {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
}
 
.grid-container > * {
  flex: 1 1 300px;
}
 
/* Enhanced layout for browsers supporting CSS Grid */
@supports (display: grid) {
  .grid-container {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 1.5rem;
  }
 
  .grid-container > * {
    flex: none;
  }
}
 
/* Further enhancement with subgrid support */
@supports (grid-template-columns: subgrid) {
  .grid-container {
    grid-template-columns: subgrid;
  }
}

Visual Regression Testing

Implement visual regression testing to catch unintended layout shifts and styling changes. Tools like Percy, Chromatic, or Playwright's screenshot comparison can detect visual differences across browsers and screen sizes:

const { test, expect } = require('@playwright/test');
 
test('responsive layout matches design', async ({ page }) => {
  await page.goto('/components/dashboard');
 
  // Test at multiple viewport sizes
  for (const viewport of [
    { width: 375, height: 812, name: 'mobile' },
    { width: 768, height: 1024, name: 'tablet' },
    { width: 1440, height: 900, name: 'desktop' },
  ]) {
    await page.setViewportSize({ width: viewport.width, height: viewport.height });
    await expect(page).toHaveScreenshot(
      `dashboard-${viewport.name}.png`,
      { maxDiffPixels: 100 }
    );
  }
});

Browser Compatibility Testing Matrix

Maintain a testing matrix that covers the browsers and versions your users actually use. Use analytics data to determine your browser support baseline, then configure tools like Browserslist to automatically handle polyfilling and prefixing:

{
  "browserslist": [
    "> 0.5%",
    "last 2 versions",
    "not dead",
    "not ie 11"
  ]
}

This data-driven approach ensures you're spending testing effort where it matters most, rather than trying to support every possible browser configuration.

Community Resources and Further Learning

The technology landscape evolves rapidly, making continuous learning essential for maintaining expertise. Building a systematic approach to staying current with developments in your technology stack ensures you can leverage new features and avoid deprecated patterns.

Curated Learning Pathways

Rather than consuming content randomly, create structured learning pathways aligned with your current projects and career goals. Start with official documentation and specification documents, which provide the most accurate and comprehensive information. Follow this with hands-on tutorials and workshops that reinforce concepts through practical application.

Technical blogs from framework maintainers and core team members often provide deeper insights into design decisions and upcoming features. Subscribe to the official blogs of your primary frameworks and libraries to stay ahead of breaking changes and deprecation timelines.

Contributing to Open Source

Contributing to open-source projects in your technology stack provides unparalleled learning opportunities. Start with documentation improvements and bug reports, then progress to fixing small issues tagged as "good first issue" in your favorite projects. This direct engagement with maintainers and the codebase accelerates your understanding far beyond what passive learning can achieve.

# Setting up for contribution
git clone https://github.com/project/repository.git
cd repository
git checkout -b fix/issue-description
 
# Run the project's contribution setup
npm run setup:dev
npm run test  # Ensure tests pass before making changes
 
# Make your changes, then run the full test suite
npm run test:full
npm run lint
npm run build
 
# Submit your contribution
git add -A
git commit -m "fix: description of the fix
 
Closes #1234"
git push origin fix/issue-description

Building a Technical Knowledge Base

Maintain a personal knowledge base that captures insights, solutions, and patterns you discover during your work. Tools like Obsidian, Notion, or even a simple Markdown repository can serve as an external memory that grows more valuable over time.

Organize your notes by topic rather than chronologically, and include code examples, links to relevant documentation, and explanations of why certain approaches work better than others. When you encounter a particularly insightful article or conference talk, write a summary that captures the key takeaways and how they apply to your current projects.

Follow key conferences and their published talks to stay informed about emerging patterns and best practices. Many conferences publish recorded talks on YouTube within weeks of the event, making world-class technical content freely accessible.

Join relevant Discord servers, Slack communities, and forums where practitioners discuss real-world challenges and solutions. These communities provide early warning about emerging issues and access to collective wisdom that isn't available through formal documentation.

Mentorship and Knowledge Sharing

Teaching others is one of the most effective ways to deepen your own understanding. Consider writing technical blog posts, giving talks at local meetups, or mentoring junior developers. The process of explaining concepts to others forces you to organize your knowledge and identify gaps in your understanding.

Pair programming sessions with colleagues of different experience levels create mutual learning opportunities. Senior developers gain fresh perspectives on problems they've solved the same way for years, while junior developers benefit from exposure to production-grade thinking and decision-making processes.

Conclusion

Framer Motion (Motion) provides a comprehensive animation system for React that handles everything from simple hover effects to complex page transitions. Its declarative API, physics-based animations, and automatic layout animations make it possible to create polished, performant animations with minimal code. Whether you're building a landing page with scroll-triggered animations, a dashboard with interactive charts, or a mobile app with gesture-based navigation, Motion has the tools you need.