Skip to content
Learni
View all tutorials
Développement Frontend

How to Master Advanced Framer Motion in 2026

Lire en français

Introduction

Framer Motion is the most powerful animation library for React in 2026, outshining native CSS APIs with its simple declarative syntax and GPU-accelerated performance. For advanced developers, it shines in complex scenarios: responsive layout animations, smooth touch gestures, scroll-linked transitions, and orchestrated element groups.

Why use it? Animations aren't just eye candy anymore—they guide users, enhance accessibility (via useReducedMotion), and improve Core Web Vitals like CLS (Cumulative Layout Shift). This advanced tutorial walks you through everything step by step, from setup to production-ready implementations with complete, functional TypeScript code. By the end, you'll build UIs that rival Figma or Notion. Ready to turn static components into immersive experiences?

Prerequisites

  • Node.js 20+ and npm/yarn/pnpm
  • React 18+ with Vite or Next.js
  • Advanced TypeScript and React hooks knowledge
  • An editor like VS Code with Framer Motion extension (optional)
  • CodeSandbox or StackBlitz for instant testing

Project Setup and Installation

terminal
npm create vite@latest framer-motion-advanced -- --template react-ts
cd framer-motion-advanced
npm install
npm install framer-motion@latest
npm run dev

This script sets up a Vite React + TypeScript project, installs the latest Framer Motion (version 11+ in 2026), and starts the dev server. Vite delivers ultra-fast HMR for iterating on animations without full rebuilds. Skip Create React App—it's too slow for animated previews.

Advanced motion components

motion.* components replace div, span, etc., adding props like animate and whileHover. For advanced use, focus on layout to smoothly animate size/position changes without custom JavaScript, preventing layout thrashing.

Basic layout animations

src/App.tsx
import { useState } from 'react';
import { motion } from 'framer-motion';

function App() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div style={{ padding: '2rem' }}>
      <motion.button
        onClick={() => setIsOpen(!isOpen)}
        style={{ marginBottom: '1rem' }}
      >
        Toggle
      </motion.button>
      <motion.div
        layout
        style={{
          width: isOpen ? 400 : 200,
          height: 200,
          backgroundColor: '#ff6b6b',
          borderRadius: 8
        }}
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        transition={{ duration: 0.3 }}
      />
    </div>
  );
}

export default App;

import ReactDOM from 'react-dom/client';
import App from './App.tsx';

ReactDOM.createRoot(document.getElementById('root')!).render(<App />);

This component showcases layout: the element expands fluidly without jumps. initial and animate handle the entrance. Pitfall: without layout, DOM changes break the animation—always add it for responsive designs. Copy-paste into Vite to test.

Variants and orchestrated transitions

Variants centralize states (open/closed) like a conductor. transition refines easing, delay, and stagger for natural effects. Think of a hamburger menu where lines stagger into an X.

Menu with variants and stagger

src/App.tsx
import { useState } from 'react';
import { motion } from 'framer-motion';

const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1
    }
  }
};

const itemVariants = {
  hidden: { y: -20, opacity: 0 },
  visible: {
    y: 0,
    opacity: 1,
    transition: { duration: 0.3, ease: 'easeOut' }
  }
};

function App() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div style={{ padding: '2rem' }}>
      <motion.button
        onClick={() => setIsOpen(!isOpen)}
        style={{ marginBottom: '1rem' }}
        whileHover={{ scale: 1.05 }}
        whileTap={{ scale: 0.95 }}
      >
        Menu
      </motion.button>
      <motion.ul
        layout
        variants={containerVariants}
        initial="hidden"
        animate={isOpen ? 'visible' : 'hidden'}
        style={{ listStyle: 'none', padding: 0 }}
      >
        <motion.li variants={itemVariants}>Item 1</motion.li>
        <motion.li variants={itemVariants}>Item 2</motion.li>
        <motion.li variants={itemVariants}>Item 3</motion.li>
      </motion.ul>
    </div>
  );
}

export default App;

import ReactDOM from 'react-dom/client';
import App from './App.tsx';

ReactDOM.createRoot(document.getElementById('root')!).render(<App />);

staggerChildren orchestrates child elements like a ballet. whileHover/tap adds tactile feedback. Pitfall: forgetting custom for child props—here it's implicit via variants. Perfect for modals or accordions.

Advanced gestures with drag

src/App.tsx
import { motion } from 'framer-motion';

function App() {
  return (
    <div style={{ padding: '2rem', height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      <motion.div
        drag
        dragConstraints={{ top: -50, left: -50, right: 50, bottom: 50 }}
        dragElastic={0.2}
        whileDrag={{ scale: 1.1, rotate: 5 }}
        style={{
          width: 100,
          height: 100,
          backgroundColor: '#4ecdc4',
          borderRadius: 12,
          cursor: 'grab'
        }}
      />
    </div>
  );
}

export default App;

import ReactDOM from 'react-dom/client';
import App from './App.tsx';

ReactDOM.createRoot(document.getElementById('root')!).render(<App />);

drag enables native cross-platform dragging (touch/mouse). dragConstraints limits to a viewport, dragElastic adds bounce. whileDrag animates during interaction. Pitfall: without cursor: grab, UX feels off—always include for mobile-first.

Scroll-linked animations

With useScroll and useTransform, tie CSS properties to scroll progress for pro parallax effects. Perfect for hero sections or timelines: fade-ins on scroll without verbose IntersectionObserver.

Scroll-triggered parallax

src/App.tsx
import { useScroll, useTransform } from 'framer-motion';
import { useRef } from 'react';
import { motion } from 'framer-motion';

function App() {
  const ref = useRef(null);
  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ['start end', 'end start']
  });
  const y = useTransform(scrollYProgress, [0, 1], [0, -200]);

  return (
    <div style={{ height: '200vh' }}>
      <section style={{ height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
        <h1 style={{ fontSize: '3rem' }}>Scroll down!</h1>
      </section>
      <motion.section
        ref={ref}
        style={{
          height: '100vh',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          y
        }}
      >
        <motion.div
          style={{
            width: 300,
            height: 300,
            backgroundColor: '#45b7d1',
            borderRadius: 20
          }}
          animate={{ opacity: scrollYProgress }}
        />
      </motion.section>
    </div>
  );
}

export default App;

import ReactDOM from 'react-dom/client';
import App from './App.tsx';

ReactDOM.createRoot(document.getElementById('root')!).render(<App />);

useScroll tracks progress, useTransform maps [0,1] to values (e.g., y for parallax). offset sets trigger points. Pitfall: forgetting ref on target—animation won't trigger. Optimized for 60fps even on mobile.

AnimatePresence with exits

src/App.tsx
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';

function App() {
  const [items, setItems] = useState(['1', '2', '3']);

  const handleAdd = () => setItems((prev) => [...prev, `${prev.length + 1}`]);
  const handleRemove = (index: number) => setItems((prev) => prev.filter((_, i) => i !== index));

  return (
    <div style={{ padding: '2rem' }}>
      <button onClick={handleAdd}>Add</button>
      <AnimatePresence>
        {items.map((item, index) => (
          <motion.div
            key={item}
            initial={{ opacity: 0, x: 50 }}
            animate={{ opacity: 1, x: 0 }}
            exit={{ opacity: 0, x: -50, scale: 0.8 }}
            transition={{ duration: 0.3 }}
            style={{
              padding: '1rem',
              margin: '0.5rem 0',
              backgroundColor: '#f0932b',
              borderRadius: 8
            }}
            onClick={() => handleRemove(index)}
          >
            Item {item}
          </motion.div>
        ))}
      </AnimatePresence>
    </div>
  );
}

export default App;

import ReactDOM from 'react-dom/client';
import App from './App.tsx';

ReactDOM.createRoot(document.getElementById('root')!).render(<App />);

AnimatePresence animates exits on unmount, crucial for dynamic lists. Unique key triggers animations. Pitfall: without it, exits are instant—ruins UX. Use for toasts or carousels.

Best practices

  • Always use layoutId for shared layouts across routes (Next.js).
  • Honor prefers-reduced-motion with for accessibility.
  • Bundle framer-motion/layout for tree-shaking.
  • Test on mobile: dragPropagation={false}` prevents conflicts.
  • Profile with React DevTools Profiler to catch excessive re-renders.

Common errors to avoid

  • Forgetting layout: causes CLS that hurts SEO.
  • Inline animate without variants: repetitive, hard-to-maintain code.
  • Ignoring mode="wait" in AnimatePresence: chaotic overlaps.
  • useTransform without precise offset: unpredictable scroll animations.

Next steps