Back to docs
14Week 2

Framer Motion

Production-quality animations — from micro-interactions to complex choreography

Installation

npm install framer-motion

Framer Motion is a production-ready animation library for React. It provides a declarative API for animations, gestures, and layout transitions that would take hundreds of lines of CSS or vanilla JavaScript to achieve.

Core Concepts

Motion Components

Framer Motion works by replacing standard HTML elements with motion equivalents. A <div> becomes <motion.div>, a <button> becomes <motion.button>, and so on.

import { motion } from "framer-motion"

// Instead of:
<div className="card">...</div>

// Use:
<motion.div className="card">...</motion.div>

Every motion component accepts animation props like initial, animate, exit, transition, whileHover, and whileTap.

Pattern 1 — Fade In on Mount

The simplest animation: fade an element in when it appears on the page.

<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.5 }}
>
  <h1>Welcome</h1>
</motion.div>

Reusable FadeIn Component

Wrap this pattern in a reusable component that respects the user's motion preferences:

"use client"

import { motion, useReducedMotion } from "framer-motion"
import { ReactNode } from "react"

interface FadeInProps {
  children: ReactNode
  delay?: number
  duration?: number
  className?: string
}

export function FadeIn({
  children,
  delay = 0,
  duration = 0.5,
  className,
}: FadeInProps) {
  const shouldReduceMotion = useReducedMotion()

  return (
    <motion.div
      initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{
        duration: shouldReduceMotion ? 0 : duration,
        delay: shouldReduceMotion ? 0 : delay,
        ease: "easeOut",
      }}
      className={className}
    >
      {children}
    </motion.div>
  )
}

// Usage:
// <FadeIn>
//   <Card />
// </FadeIn>
//
// <FadeIn delay={0.2}>
//   <Card />
// </FadeIn>

Pattern 2 — Hover & Tap Effects

Micro-interactions give your UI a polished, responsive feel. Framer Motion makes hover and tap effects trivial.

Button Press

<motion.button
  whileHover={{ scale: 1.05 }}
  whileTap={{ scale: 0.95 }}
  transition={{ type: "spring", stiffness: 400, damping: 17 }}
  className="bg-action-primary text-white px-6 py-3 rounded-button"
>
  Click Me
</motion.button>

Card Lift

<motion.div
  whileHover={{
    y: -8,
    boxShadow: "0 20px 25px -5px rgba(0, 0, 0, 0.1)",
  }}
  transition={{ type: "spring", stiffness: 300, damping: 20 }}
  className="bg-white rounded-card p-6 border"
>
  <h3>Hover to lift</h3>
  <p>This card rises on hover with a spring animation.</p>
</motion.div>

Icon Rotation

<motion.div
  whileHover={{ rotate: 90 }}
  transition={{ type: "spring", stiffness: 200, damping: 10 }}
>
  <SettingsIcon className="w-6 h-6" />
</motion.div>

Pattern 3 — Staggered List

When rendering a list of items, stagger their entrance so they appear one after another instead of all at once.

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

const itemVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: {
    opacity: 1,
    y: 0,
    transition: {
      duration: 0.4,
      ease: "easeOut",
    },
  },
}

CardGrid Example

function CardGrid({ cards }: { cards: CardData[] }) {
  return (
    <motion.div
      variants={containerVariants}
      initial="hidden"
      animate="visible"
      className="grid grid-cols-1 md:grid-cols-3 gap-6"
    >
      {cards.map((card) => (
        <motion.div
          key={card.id}
          variants={itemVariants}
          className="bg-white rounded-card p-6 border"
        >
          <h3 className="font-semibold">{card.title}</h3>
          <p className="text-text-secondary mt-2">{card.description}</p>
        </motion.div>
      ))}
    </motion.div>
  )
}

AnimatePresence — Exit Animations

By default, React removes components from the DOM immediately. AnimatePresence lets you animate components out before they are removed.

Toast Notification Example

import { motion, AnimatePresence } from "framer-motion"
import { useState } from "react"

function ToastDemo() {
  const [toasts, setToasts] = useState<string[]>([])

  const addToast = () => {
    const id = Date.now().toString()
    setToasts((prev) => [...prev, id])
    setTimeout(() => {
      setToasts((prev) => prev.filter((t) => t !== id))
    }, 3000)
  }

  return (
    <div>
      <button onClick={addToast}>Show Toast</button>

      <div className="fixed bottom-4 right-4 space-y-2">
        <AnimatePresence>
          {toasts.map((id) => (
            <motion.div
              key={id}
              initial={{ opacity: 0, x: 100 }}
              animate={{ opacity: 1, x: 0 }}
              exit={{ opacity: 0, x: 100 }}
              transition={{ type: "spring", damping: 25, stiffness: 300 }}
              className="bg-gray-900 text-white px-4 py-3 rounded-lg shadow-lg"
            >
              This is a toast notification
            </motion.div>
          ))}
        </AnimatePresence>
      </div>
    </div>
  )
}

Motion Design Principles

Duration Guidelines

DurationCategoryUse For
0–100msMicroButton presses, toggles, color changes
100–300msShortTooltips, dropdowns, fade-ins
300–500msMediumPage transitions, modals, expanding panels
500ms+Too SlowAvoid. Users perceive delays over 500ms as laggy. Only use for decorative animations that don't block interaction.

Easing Guidelines

EasingWhen to UseFramer Motion Syntax
easeOutElements entering the screen (fast start, gentle stop){ ease: "easeOut" }
easeInElements leaving the screen (gentle start, fast exit){ ease: "easeIn" }
easeInOutElements moving on screen (smooth both ends){ ease: "easeInOut" }
springInteractive elements (buttons, cards, drags){ type: "spring", stiffness: 300, damping: 20 }
linearProgress bars, loading spinners (constant speed){ ease: "linear" }

Accessibility — prefers-reduced-motion

Some users experience motion sickness or discomfort from animations. The prefers-reduced-motion media query lets you respect their system setting.

Using useReducedMotion

import { motion, useReducedMotion } from "framer-motion"

function AnimatedCard({ children }: { children: React.ReactNode }) {
  const shouldReduceMotion = useReducedMotion()

  return (
    <motion.div
      initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{
        duration: shouldReduceMotion ? 0 : 0.4,
      }}
      whileHover={shouldReduceMotion ? {} : { y: -4 }}
    >
      {children}
    </motion.div>
  )
}

CSS Media Query

For CSS-based animations, use the media query directly:

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

Always test your animations with reduced motion enabled. On macOS, go to System Settings → Accessibility → Display → Reduce motion.