Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds
Volver a colecciones
Guide28 min read

Framer Motion

Framer Motion is the most popular animation library for React. It offers a simple API, gesture support, layout animations, AnimatePresence for exit animations and variants for complex sequences.

Framer Motion - Profesjonalne Animacje w React

Czym jest Framer Motion?

Framer Motion to produkcyjna biblioteka animacji dla React, stworzona przez zespół Framer. Wyróżnia się:

  • Deklaratywnym API - animacje opisujesz w JSX
  • Gesture support - drag, hover, tap, pan
  • Layout animations - płynne przejścia między layoutami
  • Exit animations - animacje przy odmontowywaniu komponentów
  • Server-safe - działa z SSR (Next.js)
  • Variants - orkiestracja złożonych animacji

Framer Motion to standard dla animacji w ekosystemie React, używany przez Vercel, Stripe, Netflix i tysiące innych firm.

Dlaczego Framer Motion?

CSS Animations vs Framer Motion

Code
TypeScript
// ❌ CSS - ograniczone możliwości
// Brak: physics-based springs, exit animations,
// layout animations, gesture support

// ✅ Framer Motion - pełna kontrola
<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  exit={{ opacity: 0, y: -20 }}
  whileHover={{ scale: 1.05 }}
  whileTap={{ scale: 0.95 }}
  drag="x"
  dragConstraints={{ left: -100, right: 100 }}
  layout
/>

Instalacja

Code
Bash
npm install framer-motion

Podstawy Animacji

motion Component

Code
TypeScript
import { motion } from 'framer-motion'

function AnimatedBox() {
  return (
    <motion.div
      // Stan początkowy
      initial={{ opacity: 0, scale: 0.8 }}
      // Stan docelowy
      animate={{ opacity: 1, scale: 1 }}
      // Konfiguracja przejścia
      transition={{ duration: 0.5, ease: 'easeOut' }}
      className="w-32 h-32 bg-blue-500 rounded-lg"
    />
  )
}

Animowane elementy HTML

Code
TypeScript
// Wszystkie elementy HTML mają swoje motion odpowiedniki
<motion.div />
<motion.span />
<motion.button />
<motion.ul />
<motion.li />
<motion.svg />
<motion.path />
<motion.img />
<motion.a />
// ... i wszystkie inne

Animowane wartości

Code
TypeScript
// Wspierane właściwości
<motion.div
  animate={{
    // Pozycja
    x: 100,
    y: 50,

    // Skala
    scale: 1.2,
    scaleX: 1.5,
    scaleY: 0.8,

    // Obrót
    rotate: 180,
    rotateX: 45,
    rotateY: 45,
    rotateZ: 90,

    // Przezroczystość
    opacity: 0.5,

    // Kolory
    backgroundColor: '#ff0000',
    color: '#ffffff',
    borderColor: '#000000',

    // Border radius
    borderRadius: '50%',

    // Cienie
    boxShadow: '0 10px 20px rgba(0,0,0,0.3)',

    // Wymiary
    width: '200px',
    height: '100px',

    // Padding, margin
    padding: 20,
    margin: 10,
  }}
/>

Transitions - Rodzaje Przejść

Spring (domyślne)

Code
TypeScript
<motion.div
  animate={{ x: 100 }}
  transition={{
    type: 'spring',
    stiffness: 100,    // Sztywność sprężyny
    damping: 10,       // Tłumienie
    mass: 1,           // Masa
    velocity: 2,       // Początkowa prędkość
    restDelta: 0.01,   // Próg zatrzymania
    restSpeed: 0.01,
  }}
/>

Tween (klasyczne easing)

Code
TypeScript
<motion.div
  animate={{ x: 100 }}
  transition={{
    type: 'tween',
    duration: 0.5,
    ease: 'easeInOut',
    // Lub custom cubic-bezier
    ease: [0.6, 0.01, 0.05, 0.95],
  }}
/>

// Dostępne easing:
// 'linear', 'easeIn', 'easeOut', 'easeInOut'
// 'circIn', 'circOut', 'circInOut'
// 'backIn', 'backOut', 'backInOut'
// 'anticipate'

Inertia (dla drag)

Code
TypeScript
<motion.div
  drag
  dragTransition={{
    type: 'inertia',
    power: 0.8,        // Siła inercji
    timeConstant: 700, // Czas trwania
    modifyTarget: (target) => Math.round(target / 100) * 100, // Snap do grid
  }}
/>

Delay i Stagger

Code
TypeScript
<motion.div
  animate={{ x: 100 }}
  transition={{
    delay: 0.5,        // Opóźnienie startu
    delayChildren: 0.3, // Opóźnienie dla dzieci
    staggerChildren: 0.1, // Odstęp między dziećmi
    staggerDirection: 1, // 1 lub -1
  }}
/>

Interaktywność - Gestures

Hover i Tap

Code
TypeScript
<motion.button
  whileHover={{
    scale: 1.05,
    backgroundColor: '#3b82f6',
  }}
  whileTap={{
    scale: 0.95,
  }}
  transition={{ type: 'spring', stiffness: 400, damping: 17 }}
  className="px-6 py-3 bg-blue-500 text-white rounded-lg"
>
  Kliknij mnie
</motion.button>

Focus

Code
TypeScript
<motion.input
  whileFocus={{
    scale: 1.02,
    boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.5)',
  }}
  className="px-4 py-2 border rounded"
  placeholder="Wpisz tekst..."
/>

Drag

Code
TypeScript
function DraggableCard() {
  return (
    <motion.div
      drag                    // Włącz drag (true, "x", "y")
      dragConstraints={{      // Ograniczenia
        top: -100,
        left: -100,
        right: 100,
        bottom: 100,
      }}
      dragElastic={0.2}       // Elastyczność na granicy (0-1)
      dragMomentum={true}     // Inercja po puszczeniu
      dragSnapToOrigin        // Wróć do pozycji początkowej
      onDragStart={(event, info) => console.log('Start:', info.point)}
      onDrag={(event, info) => console.log('Drag:', info.offset)}
      onDragEnd={(event, info) => console.log('End:', info.velocity)}
      className="w-32 h-32 bg-purple-500 rounded-lg cursor-grab active:cursor-grabbing"
    />
  )
}

Drag z constraints ref

Code
TypeScript
function DragInContainer() {
  const constraintsRef = useRef(null)

  return (
    <motion.div
      ref={constraintsRef}
      className="w-96 h-96 bg-gray-100 rounded-xl overflow-hidden"
    >
      <motion.div
        drag
        dragConstraints={constraintsRef}
        className="w-20 h-20 bg-blue-500 rounded-lg"
      />
    </motion.div>
  )
}

Pan Gesture

Code
TypeScript
<motion.div
  onPan={(event, info) => {
    console.log('Delta:', info.delta)
    console.log('Offset:', info.offset)
    console.log('Velocity:', info.velocity)
  }}
  onPanStart={(event, info) => console.log('Pan started')}
  onPanEnd={(event, info) => console.log('Pan ended')}
/>

Variants - Orkiestracja Animacji

Podstawowe variants

Code
TypeScript
const boxVariants = {
  hidden: {
    opacity: 0,
    y: 20,
  },
  visible: {
    opacity: 1,
    y: 0,
    transition: {
      duration: 0.5,
    },
  },
  hover: {
    scale: 1.05,
    boxShadow: '0 10px 30px rgba(0,0,0,0.2)',
  },
}

function AnimatedCard() {
  return (
    <motion.div
      variants={boxVariants}
      initial="hidden"
      animate="visible"
      whileHover="hover"
      className="p-6 bg-white rounded-lg shadow"
    >
      Card content
    </motion.div>
  )
}

Propagacja do dzieci

Code
TypeScript
const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1,  // Odstęp między dziećmi
      delayChildren: 0.2,    // Opóźnienie przed pierwszym dzieckiem
    },
  },
}

const itemVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: {
    opacity: 1,
    y: 0,
    transition: { duration: 0.3 },
  },
}

function StaggeredList({ items }) {
  return (
    <motion.ul
      variants={containerVariants}
      initial="hidden"
      animate="visible"
      className="space-y-4"
    >
      {items.map((item) => (
        <motion.li
          key={item.id}
          variants={itemVariants}
          className="p-4 bg-white rounded shadow"
        >
          {item.name}
        </motion.li>
      ))}
    </motion.ul>
  )
}

Dynamic Variants

Code
TypeScript
const itemVariants = {
  hidden: (custom: number) => ({
    opacity: 0,
    x: custom * 50,
  }),
  visible: (custom: number) => ({
    opacity: 1,
    x: 0,
    transition: {
      delay: custom * 0.1,
    },
  }),
}

function DynamicList({ items }) {
  return (
    <motion.ul initial="hidden" animate="visible">
      {items.map((item, index) => (
        <motion.li
          key={item.id}
          custom={index}  // Przekaż custom value
          variants={itemVariants}
        >
          {item.name}
        </motion.li>
      ))}
    </motion.ul>
  )
}

AnimatePresence - Exit Animations

Podstawowe użycie

Code
TypeScript
import { motion, AnimatePresence } from 'framer-motion'

function Modal({ isOpen, onClose }) {
  return (
    <AnimatePresence>
      {isOpen && (
        <motion.div
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          className="fixed inset-0 bg-black/50 flex items-center justify-center"
          onClick={onClose}
        >
          <motion.div
            initial={{ scale: 0.9, opacity: 0 }}
            animate={{ scale: 1, opacity: 1 }}
            exit={{ scale: 0.9, opacity: 0 }}
            transition={{ type: 'spring', damping: 20 }}
            className="bg-white p-8 rounded-xl max-w-md w-full"
            onClick={(e) => e.stopPropagation()}
          >
            <h2>Modal Title</h2>
            <p>Modal content goes here...</p>
            <button onClick={onClose}>Close</button>
          </motion.div>
        </motion.div>
      )}
    </AnimatePresence>
  )
}

Mode dla listy

Code
TypeScript
function NotificationList({ notifications }) {
  return (
    <AnimatePresence mode="popLayout">
      {notifications.map((notification) => (
        <motion.div
          key={notification.id}
          initial={{ opacity: 0, height: 0 }}
          animate={{ opacity: 1, height: 'auto' }}
          exit={{ opacity: 0, height: 0 }}
          className="mb-2"
        >
          <Notification {...notification} />
        </motion.div>
      ))}
    </AnimatePresence>
  )
}

// Tryby AnimatePresence:
// "sync" (domyślny) - exit i enter jednocześnie
// "wait" - czekaj aż exit się skończy
// "popLayout" - automatycznie zarządza layoutem

Page Transitions (Next.js)

TSapp/template.tsx
TypeScript
// app/template.tsx
'use client'

import { motion, AnimatePresence } from 'framer-motion'
import { usePathname } from 'next/navigation'

export default function Template({ children }: { children: React.ReactNode }) {
  const pathname = usePathname()

  return (
    <AnimatePresence mode="wait">
      <motion.div
        key={pathname}
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        exit={{ opacity: 0, y: -20 }}
        transition={{ duration: 0.3 }}
      >
        {children}
      </motion.div>
    </AnimatePresence>
  )
}

Layout Animations

Podstawowe layout

Code
TypeScript
function ExpandableCard() {
  const [isExpanded, setIsExpanded] = useState(false)

  return (
    <motion.div
      layout
      onClick={() => setIsExpanded(!isExpanded)}
      className={`bg-blue-500 rounded-lg p-4 cursor-pointer ${
        isExpanded ? 'w-64 h-64' : 'w-32 h-32'
      }`}
    >
      <motion.h2 layout="position">Title</motion.h2>
      {isExpanded && (
        <motion.p
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
        >
          Expanded content...
        </motion.p>
      )}
    </motion.div>
  )
}

Shared Layout Animation

Code
TypeScript
function Tabs() {
  const [selected, setSelected] = useState(0)
  const tabs = ['Home', 'About', 'Contact']

  return (
    <div className="flex gap-2">
      {tabs.map((tab, index) => (
        <button
          key={tab}
          onClick={() => setSelected(index)}
          className="relative px-4 py-2"
        >
          {tab}
          {selected === index && (
            <motion.div
              layoutId="underline"
              className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-500"
              transition={{ type: 'spring', stiffness: 500, damping: 30 }}
            />
          )}
        </button>
      ))}
    </div>
  )
}

LayoutGroup

Code
TypeScript
import { LayoutGroup } from 'framer-motion'

function CardGrid() {
  const [cards, setCards] = useState(initialCards)

  return (
    <LayoutGroup>
      <div className="grid grid-cols-3 gap-4">
        {cards.map((card) => (
          <motion.div
            key={card.id}
            layout
            layoutId={`card-${card.id}`}
            className="p-4 bg-white rounded shadow"
          >
            {card.title}
          </motion.div>
        ))}
      </div>
    </LayoutGroup>
  )
}

Keyframes

Sekwencja wartości

Code
TypeScript
<motion.div
  animate={{
    scale: [1, 1.2, 1.2, 1, 1],
    rotate: [0, 0, 180, 180, 0],
    borderRadius: ['0%', '0%', '50%', '50%', '0%'],
  }}
  transition={{
    duration: 2,
    ease: 'easeInOut',
    times: [0, 0.2, 0.5, 0.8, 1], // Kontrola timing
    repeat: Infinity,
    repeatDelay: 1,
  }}
/>

Keyframes z obiektami

Code
TypeScript
<motion.div
  animate={{
    x: [0, 100, 0],
    backgroundColor: ['#ff0000', '#00ff00', '#0000ff'],
  }}
  transition={{
    duration: 3,
    repeat: Infinity,
  }}
/>

useAnimate Hook

Programowe animacje

Code
TypeScript
import { useAnimate } from 'framer-motion'

function AnimatedComponent() {
  const [scope, animate] = useAnimate()

  async function handleClick() {
    // Sekwencja animacji
    await animate(scope.current, { scale: 1.2 }, { duration: 0.2 })
    await animate(scope.current, { rotate: 180 }, { duration: 0.3 })
    await animate(scope.current, { scale: 1, rotate: 0 }, { duration: 0.2 })
  }

  return (
    <div ref={scope}>
      <motion.div
        className="w-32 h-32 bg-blue-500 rounded-lg"
        onClick={handleClick}
      />
    </div>
  )
}

Animowanie wielu elementów

Code
TypeScript
function StaggeredAnimation() {
  const [scope, animate] = useAnimate()

  async function handleAnimate() {
    await animate(
      'li',                           // Selektor
      { opacity: 1, x: 0 },          // Animacja
      { delay: stagger(0.1) }        // Opcje
    )
  }

  return (
    <ul ref={scope}>
      <li style={{ opacity: 0, x: -20 }}>Item 1</li>
      <li style={{ opacity: 0, x: -20 }}>Item 2</li>
      <li style={{ opacity: 0, x: -20 }}>Item 3</li>
    </ul>
  )
}

useMotionValue i useTransform

Śledzenie wartości

Code
TypeScript
import { motion, useMotionValue, useTransform } from 'framer-motion'

function ParallaxCard() {
  const x = useMotionValue(0)
  const y = useMotionValue(0)

  // Transformuj wartości
  const rotateX = useTransform(y, [-100, 100], [30, -30])
  const rotateY = useTransform(x, [-100, 100], [-30, 30])

  return (
    <motion.div
      style={{ x, y, rotateX, rotateY }}
      drag
      dragElastic={0.1}
      dragConstraints={{ top: 0, left: 0, right: 0, bottom: 0 }}
      className="w-64 h-80 bg-gradient-to-br from-purple-500 to-pink-500 rounded-xl"
    />
  )
}

useMotionValueEvent

Code
TypeScript
import { useMotionValue, useMotionValueEvent } from 'framer-motion'

function ScrollProgress() {
  const scrollY = useMotionValue(0)

  useMotionValueEvent(scrollY, 'change', (latest) => {
    console.log('Scroll position:', latest)
  })

  return (/* ... */)
}

useScroll Hook

Scroll progress

Code
TypeScript
import { motion, useScroll, useTransform } from 'framer-motion'

function ScrollProgressBar() {
  const { scrollYProgress } = useScroll()

  return (
    <motion.div
      className="fixed top-0 left-0 right-0 h-1 bg-blue-500 origin-left"
      style={{ scaleX: scrollYProgress }}
    />
  )
}

Scroll-linked animations

Code
TypeScript
function ParallaxSection() {
  const ref = useRef(null)
  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ['start end', 'end start'],
  })

  const y = useTransform(scrollYProgress, [0, 1], [100, -100])
  const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [0, 1, 0])

  return (
    <div ref={ref} className="h-screen relative overflow-hidden">
      <motion.div
        style={{ y, opacity }}
        className="absolute inset-0 flex items-center justify-center"
      >
        <h1 className="text-6xl font-bold">Parallax Text</h1>
      </motion.div>
    </div>
  )
}

Scroll velocity

Code
TypeScript
import { useScroll, useVelocity, useTransform } from 'framer-motion'

function VelocityText() {
  const { scrollY } = useScroll()
  const scrollVelocity = useVelocity(scrollY)
  const skewX = useTransform(scrollVelocity, [-1000, 0, 1000], [-10, 0, 10])

  return (
    <motion.h1
      style={{ skewX }}
      className="text-4xl font-bold"
    >
      Velocity-based skew
    </motion.h1>
  )
}

useInView Hook

Animacja przy wejściu w viewport

Code
TypeScript
import { motion, useInView } from 'framer-motion'
import { useRef } from 'react'

function FadeInSection({ children }) {
  const ref = useRef(null)
  const isInView = useInView(ref, {
    once: true,        // Animuj tylko raz
    margin: '-100px',  // Offset
    amount: 0.5,       // Procent widoczności (0-1)
  })

  return (
    <motion.div
      ref={ref}
      initial={{ opacity: 0, y: 50 }}
      animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 50 }}
      transition={{ duration: 0.5 }}
    >
      {children}
    </motion.div>
  )
}

whileInView

Code
TypeScript
<motion.div
  initial={{ opacity: 0, scale: 0.8 }}
  whileInView={{ opacity: 1, scale: 1 }}
  viewport={{ once: true, amount: 0.8 }}
  transition={{ duration: 0.5 }}
>
  Content that animates when visible
</motion.div>

SVG Animations

Path animation

Code
TypeScript
function DrawPath() {
  return (
    <motion.svg width="200" height="200" viewBox="0 0 200 200">
      <motion.path
        d="M 10 80 Q 100 10 190 80 T 190 80"
        fill="transparent"
        stroke="#3b82f6"
        strokeWidth="4"
        initial={{ pathLength: 0 }}
        animate={{ pathLength: 1 }}
        transition={{ duration: 2, ease: 'easeInOut' }}
      />
    </motion.svg>
  )
}

Animated icon

Code
TypeScript
const checkmarkVariants = {
  hidden: {
    pathLength: 0,
    opacity: 0,
  },
  visible: {
    pathLength: 1,
    opacity: 1,
    transition: {
      duration: 0.5,
      ease: 'easeOut',
    },
  },
}

function AnimatedCheckmark() {
  return (
    <motion.svg
      width="50"
      height="50"
      viewBox="0 0 50 50"
      initial="hidden"
      animate="visible"
    >
      <motion.circle
        cx="25"
        cy="25"
        r="20"
        fill="none"
        stroke="#10b981"
        strokeWidth="3"
        variants={checkmarkVariants}
      />
      <motion.path
        d="M 14 25 L 22 33 L 36 17"
        fill="none"
        stroke="#10b981"
        strokeWidth="3"
        strokeLinecap="round"
        strokeLinejoin="round"
        variants={checkmarkVariants}
      />
    </motion.svg>
  )
}

Praktyczne Przykłady

Animated Modal

Code
TypeScript
const backdropVariants = {
  hidden: { opacity: 0 },
  visible: { opacity: 1 },
}

const modalVariants = {
  hidden: {
    opacity: 0,
    scale: 0.8,
    y: 50,
  },
  visible: {
    opacity: 1,
    scale: 1,
    y: 0,
    transition: {
      type: 'spring',
      damping: 25,
      stiffness: 300,
    },
  },
  exit: {
    opacity: 0,
    scale: 0.8,
    y: 50,
    transition: {
      duration: 0.2,
    },
  },
}

function Modal({ isOpen, onClose, children }) {
  return (
    <AnimatePresence>
      {isOpen && (
        <motion.div
          variants={backdropVariants}
          initial="hidden"
          animate="visible"
          exit="hidden"
          className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50"
          onClick={onClose}
        >
          <motion.div
            variants={modalVariants}
            initial="hidden"
            animate="visible"
            exit="exit"
            className="bg-white rounded-2xl p-8 max-w-lg w-full mx-4 shadow-2xl"
            onClick={(e) => e.stopPropagation()}
          >
            {children}
          </motion.div>
        </motion.div>
      )}
    </AnimatePresence>
  )
}

Animated Accordion

Code
TypeScript
function Accordion({ items }) {
  const [openIndex, setOpenIndex] = useState<number | null>(null)

  return (
    <div className="space-y-2">
      {items.map((item, index) => (
        <div key={index} className="border rounded-lg overflow-hidden">
          <button
            onClick={() => setOpenIndex(openIndex === index ? null : index)}
            className="w-full p-4 text-left flex justify-between items-center"
          >
            <span className="font-medium">{item.title}</span>
            <motion.span
              animate={{ rotate: openIndex === index ? 180 : 0 }}
              transition={{ duration: 0.2 }}
            >
            </motion.span>
          </button>

          <AnimatePresence>
            {openIndex === index && (
              <motion.div
                initial={{ height: 0, opacity: 0 }}
                animate={{ height: 'auto', opacity: 1 }}
                exit={{ height: 0, opacity: 0 }}
                transition={{ duration: 0.3 }}
              >
                <div className="p-4 pt-0 text-gray-600">
                  {item.content}
                </div>
              </motion.div>
            )}
          </AnimatePresence>
        </div>
      ))}
    </div>
  )
}

Animated Counter

Code
TypeScript
function AnimatedCounter({ value }: { value: number }) {
  const count = useMotionValue(0)
  const rounded = useTransform(count, (latest) => Math.round(latest))
  const displayValue = useTransform(rounded, (latest) =>
    latest.toLocaleString()
  )

  useEffect(() => {
    const animation = animate(count, value, {
      duration: 2,
      ease: 'easeOut',
    })

    return animation.stop
  }, [value, count])

  return (
    <motion.span className="text-4xl font-bold tabular-nums">
      {displayValue}
    </motion.span>
  )
}

Animated Toast

Code
TypeScript
const toastVariants = {
  initial: {
    opacity: 0,
    y: 50,
    scale: 0.3,
  },
  animate: {
    opacity: 1,
    y: 0,
    scale: 1,
    transition: {
      type: 'spring',
      stiffness: 400,
      damping: 25,
    },
  },
  exit: {
    opacity: 0,
    x: 100,
    transition: {
      duration: 0.2,
    },
  },
}

function ToastContainer({ toasts, removeToast }) {
  return (
    <div className="fixed bottom-4 right-4 space-y-2 z-50">
      <AnimatePresence mode="popLayout">
        {toasts.map((toast) => (
          <motion.div
            key={toast.id}
            layout
            variants={toastVariants}
            initial="initial"
            animate="animate"
            exit="exit"
            className="bg-white shadow-lg rounded-lg p-4 min-w-[300px]"
          >
            <p>{toast.message}</p>
            <button
              onClick={() => removeToast(toast.id)}
              className="absolute top-2 right-2"
            >
              ×
            </button>
          </motion.div>
        ))}
      </AnimatePresence>
    </div>
  )
}

Optymalizacja Wydajności

willChange

Code
TypeScript
// Framer Motion automatycznie dodaje will-change
// Ale możesz kontrolować to ręcznie:
<motion.div
  style={{ willChange: 'transform' }}
  animate={{ x: 100 }}
/>

Reduce Motion

Code
TypeScript
import { useReducedMotion } from 'framer-motion'

function AccessibleAnimation() {
  const shouldReduceMotion = useReducedMotion()

  return (
    <motion.div
      animate={{
        x: shouldReduceMotion ? 0 : 100,
        opacity: 1,
      }}
    />
  )
}

LazyMotion

Code
TypeScript
import { LazyMotion, domAnimation, m } from 'framer-motion'

// Mniejszy bundle - ładuje tylko używane features
function App() {
  return (
    <LazyMotion features={domAnimation}>
      <m.div animate={{ opacity: 1 }} />
    </LazyMotion>
  )
}

Framer Motion vs Alternatywy

CechaFramer MotionReact SpringGSAP
Bundle size~40KB~25KB~60KB
APIDeklaratywneHook-basedImperatywne
Exit animations✅ Native⚠️ Trudne⚠️ Manualne
Layout animations✅ Native⚠️ Plugin
Gestures✅ Wbudowane⚠️ Plugin
SSR⚠️
Learning curveŁatwyŚredniTrudny

FAQ

Czy Framer Motion działa z Next.js?

Tak! Wspiera SSR i App Router. Użyj 'use client' dla komponentów z animacjami.

Jak animować przy routingu?

Użyj AnimatePresence z template.tsx w Next.js App Router.

Czy mogę używać z TypeScript?

Tak, Framer Motion ma pełne typy TypeScript.

Jak debugować animacje?

Użyj transition={{ duration: 2 }} do spowolnienia animacji. Sprawdź DevTools Performance.

Podsumowanie

Framer Motion to najlepsza biblioteka animacji dla React:

  • Proste API - animacje w JSX
  • Kompletność - gestures, layout, exit animations
  • Wydajność - optymalizacje out of the box
  • TypeScript - pełne wsparcie
  • Aktywny rozwój - regularne aktualizacje

Jeśli budujesz aplikację React i potrzebujesz animacji - Framer Motion to standard.


Framer Motion - Professional Animations in React

What is Framer Motion?

Framer Motion is a production-ready animation library for React, created by the Framer team. It stands out with:

  • Declarative API - you describe animations in JSX
  • Gesture support - drag, hover, tap, pan
  • Layout animations - smooth transitions between layouts
  • Exit animations - animations when unmounting components
  • Server-safe - works with SSR (Next.js)
  • Variants - orchestration of complex animations

Framer Motion is the standard for animations in the React ecosystem, used by Vercel, Stripe, Netflix, and thousands of other companies.

Why Framer Motion?

CSS Animations vs Framer Motion

Code
TypeScript
// ❌ CSS - limited capabilities
// Missing: physics-based springs, exit animations,
// layout animations, gesture support

// ✅ Framer Motion - full control
<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  exit={{ opacity: 0, y: -20 }}
  whileHover={{ scale: 1.05 }}
  whileTap={{ scale: 0.95 }}
  drag="x"
  dragConstraints={{ left: -100, right: 100 }}
  layout
/>

Installation

Code
Bash
npm install framer-motion

Animation Basics

motion Component

Code
TypeScript
import { motion } from 'framer-motion'

function AnimatedBox() {
  return (
    <motion.div
      // Initial state
      initial={{ opacity: 0, scale: 0.8 }}
      // Target state
      animate={{ opacity: 1, scale: 1 }}
      // Transition configuration
      transition={{ duration: 0.5, ease: 'easeOut' }}
      className="w-32 h-32 bg-blue-500 rounded-lg"
    />
  )
}

Animated HTML elements

Code
TypeScript
// All HTML elements have their motion equivalents
<motion.div />
<motion.span />
<motion.button />
<motion.ul />
<motion.li />
<motion.svg />
<motion.path />
<motion.img />
<motion.a />
// ... and all others

Animated values

Code
TypeScript
// Supported properties
<motion.div
  animate={{
    // Position
    x: 100,
    y: 50,

    // Scale
    scale: 1.2,
    scaleX: 1.5,
    scaleY: 0.8,

    // Rotation
    rotate: 180,
    rotateX: 45,
    rotateY: 45,
    rotateZ: 90,

    // Opacity
    opacity: 0.5,

    // Colors
    backgroundColor: '#ff0000',
    color: '#ffffff',
    borderColor: '#000000',

    // Border radius
    borderRadius: '50%',

    // Shadows
    boxShadow: '0 10px 20px rgba(0,0,0,0.3)',

    // Dimensions
    width: '200px',
    height: '100px',

    // Padding, margin
    padding: 20,
    margin: 10,
  }}
/>

Transitions - Types of Transitions

Spring (default)

Code
TypeScript
<motion.div
  animate={{ x: 100 }}
  transition={{
    type: 'spring',
    stiffness: 100,    // Spring stiffness
    damping: 10,       // Damping
    mass: 1,           // Mass
    velocity: 2,       // Initial velocity
    restDelta: 0.01,   // Stopping threshold
    restSpeed: 0.01,
  }}
/>

Tween (classic easing)

Code
TypeScript
<motion.div
  animate={{ x: 100 }}
  transition={{
    type: 'tween',
    duration: 0.5,
    ease: 'easeInOut',
    // Or custom cubic-bezier
    ease: [0.6, 0.01, 0.05, 0.95],
  }}
/>

// Available easings:
// 'linear', 'easeIn', 'easeOut', 'easeInOut'
// 'circIn', 'circOut', 'circInOut'
// 'backIn', 'backOut', 'backInOut'
// 'anticipate'

Inertia (for drag)

Code
TypeScript
<motion.div
  drag
  dragTransition={{
    type: 'inertia',
    power: 0.8,        // Inertia strength
    timeConstant: 700, // Duration
    modifyTarget: (target) => Math.round(target / 100) * 100, // Snap to grid
  }}
/>

Delay and Stagger

Code
TypeScript
<motion.div
  animate={{ x: 100 }}
  transition={{
    delay: 0.5,        // Start delay
    delayChildren: 0.3, // Delay for children
    staggerChildren: 0.1, // Interval between children
    staggerDirection: 1, // 1 or -1
  }}
/>

Interactivity - Gestures

Hover and Tap

Code
TypeScript
<motion.button
  whileHover={{
    scale: 1.05,
    backgroundColor: '#3b82f6',
  }}
  whileTap={{
    scale: 0.95,
  }}
  transition={{ type: 'spring', stiffness: 400, damping: 17 }}
  className="px-6 py-3 bg-blue-500 text-white rounded-lg"
>
  Click me
</motion.button>

Focus

Code
TypeScript
<motion.input
  whileFocus={{
    scale: 1.02,
    boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.5)',
  }}
  className="px-4 py-2 border rounded"
  placeholder="Enter text..."
/>

Drag

Code
TypeScript
function DraggableCard() {
  return (
    <motion.div
      drag                    // Enable drag (true, "x", "y")
      dragConstraints={{      // Constraints
        top: -100,
        left: -100,
        right: 100,
        bottom: 100,
      }}
      dragElastic={0.2}       // Elasticity at boundary (0-1)
      dragMomentum={true}     // Inertia after release
      dragSnapToOrigin        // Return to initial position
      onDragStart={(event, info) => console.log('Start:', info.point)}
      onDrag={(event, info) => console.log('Drag:', info.offset)}
      onDragEnd={(event, info) => console.log('End:', info.velocity)}
      className="w-32 h-32 bg-purple-500 rounded-lg cursor-grab active:cursor-grabbing"
    />
  )
}

Drag with constraints ref

Code
TypeScript
function DragInContainer() {
  const constraintsRef = useRef(null)

  return (
    <motion.div
      ref={constraintsRef}
      className="w-96 h-96 bg-gray-100 rounded-xl overflow-hidden"
    >
      <motion.div
        drag
        dragConstraints={constraintsRef}
        className="w-20 h-20 bg-blue-500 rounded-lg"
      />
    </motion.div>
  )
}

Pan Gesture

Code
TypeScript
<motion.div
  onPan={(event, info) => {
    console.log('Delta:', info.delta)
    console.log('Offset:', info.offset)
    console.log('Velocity:', info.velocity)
  }}
  onPanStart={(event, info) => console.log('Pan started')}
  onPanEnd={(event, info) => console.log('Pan ended')}
/>

Variants - Animation Orchestration

Basic variants

Code
TypeScript
const boxVariants = {
  hidden: {
    opacity: 0,
    y: 20,
  },
  visible: {
    opacity: 1,
    y: 0,
    transition: {
      duration: 0.5,
    },
  },
  hover: {
    scale: 1.05,
    boxShadow: '0 10px 30px rgba(0,0,0,0.2)',
  },
}

function AnimatedCard() {
  return (
    <motion.div
      variants={boxVariants}
      initial="hidden"
      animate="visible"
      whileHover="hover"
      className="p-6 bg-white rounded-lg shadow"
    >
      Card content
    </motion.div>
  )
}

Propagation to children

Code
TypeScript
const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1,  // Interval between children
      delayChildren: 0.2,    // Delay before the first child
    },
  },
}

const itemVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: {
    opacity: 1,
    y: 0,
    transition: { duration: 0.3 },
  },
}

function StaggeredList({ items }) {
  return (
    <motion.ul
      variants={containerVariants}
      initial="hidden"
      animate="visible"
      className="space-y-4"
    >
      {items.map((item) => (
        <motion.li
          key={item.id}
          variants={itemVariants}
          className="p-4 bg-white rounded shadow"
        >
          {item.name}
        </motion.li>
      ))}
    </motion.ul>
  )
}

Dynamic Variants

Code
TypeScript
const itemVariants = {
  hidden: (custom: number) => ({
    opacity: 0,
    x: custom * 50,
  }),
  visible: (custom: number) => ({
    opacity: 1,
    x: 0,
    transition: {
      delay: custom * 0.1,
    },
  }),
}

function DynamicList({ items }) {
  return (
    <motion.ul initial="hidden" animate="visible">
      {items.map((item, index) => (
        <motion.li
          key={item.id}
          custom={index}  // Pass custom value
          variants={itemVariants}
        >
          {item.name}
        </motion.li>
      ))}
    </motion.ul>
  )
}

AnimatePresence - Exit Animations

Basic usage

Code
TypeScript
import { motion, AnimatePresence } from 'framer-motion'

function Modal({ isOpen, onClose }) {
  return (
    <AnimatePresence>
      {isOpen && (
        <motion.div
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          className="fixed inset-0 bg-black/50 flex items-center justify-center"
          onClick={onClose}
        >
          <motion.div
            initial={{ scale: 0.9, opacity: 0 }}
            animate={{ scale: 1, opacity: 1 }}
            exit={{ scale: 0.9, opacity: 0 }}
            transition={{ type: 'spring', damping: 20 }}
            className="bg-white p-8 rounded-xl max-w-md w-full"
            onClick={(e) => e.stopPropagation()}
          >
            <h2>Modal Title</h2>
            <p>Modal content goes here...</p>
            <button onClick={onClose}>Close</button>
          </motion.div>
        </motion.div>
      )}
    </AnimatePresence>
  )
}

Mode for lists

Code
TypeScript
function NotificationList({ notifications }) {
  return (
    <AnimatePresence mode="popLayout">
      {notifications.map((notification) => (
        <motion.div
          key={notification.id}
          initial={{ opacity: 0, height: 0 }}
          animate={{ opacity: 1, height: 'auto' }}
          exit={{ opacity: 0, height: 0 }}
          className="mb-2"
        >
          <Notification {...notification} />
        </motion.div>
      ))}
    </AnimatePresence>
  )
}

// AnimatePresence modes:
// "sync" (default) - exit and enter simultaneously
// "wait" - wait until exit finishes
// "popLayout" - automatically manages layout

Page Transitions (Next.js)

TSapp/template.tsx
TypeScript
// app/template.tsx
'use client'

import { motion, AnimatePresence } from 'framer-motion'
import { usePathname } from 'next/navigation'

export default function Template({ children }: { children: React.ReactNode }) {
  const pathname = usePathname()

  return (
    <AnimatePresence mode="wait">
      <motion.div
        key={pathname}
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        exit={{ opacity: 0, y: -20 }}
        transition={{ duration: 0.3 }}
      >
        {children}
      </motion.div>
    </AnimatePresence>
  )
}

Layout Animations

Basic layout

Code
TypeScript
function ExpandableCard() {
  const [isExpanded, setIsExpanded] = useState(false)

  return (
    <motion.div
      layout
      onClick={() => setIsExpanded(!isExpanded)}
      className={`bg-blue-500 rounded-lg p-4 cursor-pointer ${
        isExpanded ? 'w-64 h-64' : 'w-32 h-32'
      }`}
    >
      <motion.h2 layout="position">Title</motion.h2>
      {isExpanded && (
        <motion.p
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
        >
          Expanded content...
        </motion.p>
      )}
    </motion.div>
  )
}

Shared Layout Animation

Code
TypeScript
function Tabs() {
  const [selected, setSelected] = useState(0)
  const tabs = ['Home', 'About', 'Contact']

  return (
    <div className="flex gap-2">
      {tabs.map((tab, index) => (
        <button
          key={tab}
          onClick={() => setSelected(index)}
          className="relative px-4 py-2"
        >
          {tab}
          {selected === index && (
            <motion.div
              layoutId="underline"
              className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-500"
              transition={{ type: 'spring', stiffness: 500, damping: 30 }}
            />
          )}
        </button>
      ))}
    </div>
  )
}

LayoutGroup

Code
TypeScript
import { LayoutGroup } from 'framer-motion'

function CardGrid() {
  const [cards, setCards] = useState(initialCards)

  return (
    <LayoutGroup>
      <div className="grid grid-cols-3 gap-4">
        {cards.map((card) => (
          <motion.div
            key={card.id}
            layout
            layoutId={`card-${card.id}`}
            className="p-4 bg-white rounded shadow"
          >
            {card.title}
          </motion.div>
        ))}
      </div>
    </LayoutGroup>
  )
}

Keyframes

Value sequences

Code
TypeScript
<motion.div
  animate={{
    scale: [1, 1.2, 1.2, 1, 1],
    rotate: [0, 0, 180, 180, 0],
    borderRadius: ['0%', '0%', '50%', '50%', '0%'],
  }}
  transition={{
    duration: 2,
    ease: 'easeInOut',
    times: [0, 0.2, 0.5, 0.8, 1], // Timing control
    repeat: Infinity,
    repeatDelay: 1,
  }}
/>

Keyframes with objects

Code
TypeScript
<motion.div
  animate={{
    x: [0, 100, 0],
    backgroundColor: ['#ff0000', '#00ff00', '#0000ff'],
  }}
  transition={{
    duration: 3,
    repeat: Infinity,
  }}
/>

useAnimate Hook

Programmatic animations

Code
TypeScript
import { useAnimate } from 'framer-motion'

function AnimatedComponent() {
  const [scope, animate] = useAnimate()

  async function handleClick() {
    // Animation sequence
    await animate(scope.current, { scale: 1.2 }, { duration: 0.2 })
    await animate(scope.current, { rotate: 180 }, { duration: 0.3 })
    await animate(scope.current, { scale: 1, rotate: 0 }, { duration: 0.2 })
  }

  return (
    <div ref={scope}>
      <motion.div
        className="w-32 h-32 bg-blue-500 rounded-lg"
        onClick={handleClick}
      />
    </div>
  )
}

Animating multiple elements

Code
TypeScript
function StaggeredAnimation() {
  const [scope, animate] = useAnimate()

  async function handleAnimate() {
    await animate(
      'li',                           // Selector
      { opacity: 1, x: 0 },          // Animation
      { delay: stagger(0.1) }        // Options
    )
  }

  return (
    <ul ref={scope}>
      <li style={{ opacity: 0, x: -20 }}>Item 1</li>
      <li style={{ opacity: 0, x: -20 }}>Item 2</li>
      <li style={{ opacity: 0, x: -20 }}>Item 3</li>
    </ul>
  )
}

useMotionValue and useTransform

Tracking values

Code
TypeScript
import { motion, useMotionValue, useTransform } from 'framer-motion'

function ParallaxCard() {
  const x = useMotionValue(0)
  const y = useMotionValue(0)

  // Transform values
  const rotateX = useTransform(y, [-100, 100], [30, -30])
  const rotateY = useTransform(x, [-100, 100], [-30, 30])

  return (
    <motion.div
      style={{ x, y, rotateX, rotateY }}
      drag
      dragElastic={0.1}
      dragConstraints={{ top: 0, left: 0, right: 0, bottom: 0 }}
      className="w-64 h-80 bg-gradient-to-br from-purple-500 to-pink-500 rounded-xl"
    />
  )
}

useMotionValueEvent

Code
TypeScript
import { useMotionValue, useMotionValueEvent } from 'framer-motion'

function ScrollProgress() {
  const scrollY = useMotionValue(0)

  useMotionValueEvent(scrollY, 'change', (latest) => {
    console.log('Scroll position:', latest)
  })

  return (/* ... */)
}

useScroll Hook

Scroll progress

Code
TypeScript
import { motion, useScroll, useTransform } from 'framer-motion'

function ScrollProgressBar() {
  const { scrollYProgress } = useScroll()

  return (
    <motion.div
      className="fixed top-0 left-0 right-0 h-1 bg-blue-500 origin-left"
      style={{ scaleX: scrollYProgress }}
    />
  )
}

Scroll-linked animations

Code
TypeScript
function ParallaxSection() {
  const ref = useRef(null)
  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ['start end', 'end start'],
  })

  const y = useTransform(scrollYProgress, [0, 1], [100, -100])
  const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [0, 1, 0])

  return (
    <div ref={ref} className="h-screen relative overflow-hidden">
      <motion.div
        style={{ y, opacity }}
        className="absolute inset-0 flex items-center justify-center"
      >
        <h1 className="text-6xl font-bold">Parallax Text</h1>
      </motion.div>
    </div>
  )
}

Scroll velocity

Code
TypeScript
import { useScroll, useVelocity, useTransform } from 'framer-motion'

function VelocityText() {
  const { scrollY } = useScroll()
  const scrollVelocity = useVelocity(scrollY)
  const skewX = useTransform(scrollVelocity, [-1000, 0, 1000], [-10, 0, 10])

  return (
    <motion.h1
      style={{ skewX }}
      className="text-4xl font-bold"
    >
      Velocity-based skew
    </motion.h1>
  )
}

useInView Hook

Animation on entering the viewport

Code
TypeScript
import { motion, useInView } from 'framer-motion'
import { useRef } from 'react'

function FadeInSection({ children }) {
  const ref = useRef(null)
  const isInView = useInView(ref, {
    once: true,        // Animate only once
    margin: '-100px',  // Offset
    amount: 0.5,       // Visibility percentage (0-1)
  })

  return (
    <motion.div
      ref={ref}
      initial={{ opacity: 0, y: 50 }}
      animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 50 }}
      transition={{ duration: 0.5 }}
    >
      {children}
    </motion.div>
  )
}

whileInView

Code
TypeScript
<motion.div
  initial={{ opacity: 0, scale: 0.8 }}
  whileInView={{ opacity: 1, scale: 1 }}
  viewport={{ once: true, amount: 0.8 }}
  transition={{ duration: 0.5 }}
>
  Content that animates when visible
</motion.div>

SVG Animations

Path animation

Code
TypeScript
function DrawPath() {
  return (
    <motion.svg width="200" height="200" viewBox="0 0 200 200">
      <motion.path
        d="M 10 80 Q 100 10 190 80 T 190 80"
        fill="transparent"
        stroke="#3b82f6"
        strokeWidth="4"
        initial={{ pathLength: 0 }}
        animate={{ pathLength: 1 }}
        transition={{ duration: 2, ease: 'easeInOut' }}
      />
    </motion.svg>
  )
}

Animated icon

Code
TypeScript
const checkmarkVariants = {
  hidden: {
    pathLength: 0,
    opacity: 0,
  },
  visible: {
    pathLength: 1,
    opacity: 1,
    transition: {
      duration: 0.5,
      ease: 'easeOut',
    },
  },
}

function AnimatedCheckmark() {
  return (
    <motion.svg
      width="50"
      height="50"
      viewBox="0 0 50 50"
      initial="hidden"
      animate="visible"
    >
      <motion.circle
        cx="25"
        cy="25"
        r="20"
        fill="none"
        stroke="#10b981"
        strokeWidth="3"
        variants={checkmarkVariants}
      />
      <motion.path
        d="M 14 25 L 22 33 L 36 17"
        fill="none"
        stroke="#10b981"
        strokeWidth="3"
        strokeLinecap="round"
        strokeLinejoin="round"
        variants={checkmarkVariants}
      />
    </motion.svg>
  )
}

Practical Examples

Animated Modal

Code
TypeScript
const backdropVariants = {
  hidden: { opacity: 0 },
  visible: { opacity: 1 },
}

const modalVariants = {
  hidden: {
    opacity: 0,
    scale: 0.8,
    y: 50,
  },
  visible: {
    opacity: 1,
    scale: 1,
    y: 0,
    transition: {
      type: 'spring',
      damping: 25,
      stiffness: 300,
    },
  },
  exit: {
    opacity: 0,
    scale: 0.8,
    y: 50,
    transition: {
      duration: 0.2,
    },
  },
}

function Modal({ isOpen, onClose, children }) {
  return (
    <AnimatePresence>
      {isOpen && (
        <motion.div
          variants={backdropVariants}
          initial="hidden"
          animate="visible"
          exit="hidden"
          className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50"
          onClick={onClose}
        >
          <motion.div
            variants={modalVariants}
            initial="hidden"
            animate="visible"
            exit="exit"
            className="bg-white rounded-2xl p-8 max-w-lg w-full mx-4 shadow-2xl"
            onClick={(e) => e.stopPropagation()}
          >
            {children}
          </motion.div>
        </motion.div>
      )}
    </AnimatePresence>
  )
}

Animated Accordion

Code
TypeScript
function Accordion({ items }) {
  const [openIndex, setOpenIndex] = useState<number | null>(null)

  return (
    <div className="space-y-2">
      {items.map((item, index) => (
        <div key={index} className="border rounded-lg overflow-hidden">
          <button
            onClick={() => setOpenIndex(openIndex === index ? null : index)}
            className="w-full p-4 text-left flex justify-between items-center"
          >
            <span className="font-medium">{item.title}</span>
            <motion.span
              animate={{ rotate: openIndex === index ? 180 : 0 }}
              transition={{ duration: 0.2 }}
            >
            </motion.span>
          </button>

          <AnimatePresence>
            {openIndex === index && (
              <motion.div
                initial={{ height: 0, opacity: 0 }}
                animate={{ height: 'auto', opacity: 1 }}
                exit={{ height: 0, opacity: 0 }}
                transition={{ duration: 0.3 }}
              >
                <div className="p-4 pt-0 text-gray-600">
                  {item.content}
                </div>
              </motion.div>
            )}
          </AnimatePresence>
        </div>
      ))}
    </div>
  )
}

Animated Counter

Code
TypeScript
function AnimatedCounter({ value }: { value: number }) {
  const count = useMotionValue(0)
  const rounded = useTransform(count, (latest) => Math.round(latest))
  const displayValue = useTransform(rounded, (latest) =>
    latest.toLocaleString()
  )

  useEffect(() => {
    const animation = animate(count, value, {
      duration: 2,
      ease: 'easeOut',
    })

    return animation.stop
  }, [value, count])

  return (
    <motion.span className="text-4xl font-bold tabular-nums">
      {displayValue}
    </motion.span>
  )
}

Animated Toast

Code
TypeScript
const toastVariants = {
  initial: {
    opacity: 0,
    y: 50,
    scale: 0.3,
  },
  animate: {
    opacity: 1,
    y: 0,
    scale: 1,
    transition: {
      type: 'spring',
      stiffness: 400,
      damping: 25,
    },
  },
  exit: {
    opacity: 0,
    x: 100,
    transition: {
      duration: 0.2,
    },
  },
}

function ToastContainer({ toasts, removeToast }) {
  return (
    <div className="fixed bottom-4 right-4 space-y-2 z-50">
      <AnimatePresence mode="popLayout">
        {toasts.map((toast) => (
          <motion.div
            key={toast.id}
            layout
            variants={toastVariants}
            initial="initial"
            animate="animate"
            exit="exit"
            className="bg-white shadow-lg rounded-lg p-4 min-w-[300px]"
          >
            <p>{toast.message}</p>
            <button
              onClick={() => removeToast(toast.id)}
              className="absolute top-2 right-2"
            >
              ×
            </button>
          </motion.div>
        ))}
      </AnimatePresence>
    </div>
  )
}

Performance Optimization

willChange

Code
TypeScript
// Framer Motion automatically adds will-change
// But you can control it manually:
<motion.div
  style={{ willChange: 'transform' }}
  animate={{ x: 100 }}
/>

Reduce Motion

Code
TypeScript
import { useReducedMotion } from 'framer-motion'

function AccessibleAnimation() {
  const shouldReduceMotion = useReducedMotion()

  return (
    <motion.div
      animate={{
        x: shouldReduceMotion ? 0 : 100,
        opacity: 1,
      }}
    />
  )
}

LazyMotion

Code
TypeScript
import { LazyMotion, domAnimation, m } from 'framer-motion'

// Smaller bundle - loads only the features being used
function App() {
  return (
    <LazyMotion features={domAnimation}>
      <m.div animate={{ opacity: 1 }} />
    </LazyMotion>
  )
}

Framer Motion vs Alternatives

FeatureFramer MotionReact SpringGSAP
Bundle size~40KB~25KB~60KB
APIDeclarativeHook-basedImperative
Exit animations✅ Native⚠️ Difficult⚠️ Manual
Layout animations✅ Native⚠️ Plugin
Gestures✅ Built-in⚠️ Plugin
SSR⚠️
Learning curveEasyMediumHard

FAQ

Does Framer Motion work with Next.js?

Yes! It supports SSR and App Router. Use 'use client' for components with animations.

How to animate during routing?

Use AnimatePresence with template.tsx in Next.js App Router.

Can I use it with TypeScript?

Yes, Framer Motion has full TypeScript types.

How to debug animations?

Use transition={{ duration: 2 }} to slow down animations. Check DevTools Performance.

Summary

Framer Motion is the best animation library for React:

  • Simple API - animations in JSX
  • Completeness - gestures, layout, exit animations
  • Performance - optimizations out of the box
  • TypeScript - full support
  • Active development - regular updates

If you are building a React application and need animations - Framer Motion is the standard.