Framer Motion Guide

Complete guide to using Framer Motion for smooth, pixel-perfect animations in our Next.js project

Framer Motion Guide

Complete guide to using Framer Motion for smooth, production-grade animations in our Next.js project.

What is Framer Motion?

Framer Motion is a production-ready motion library for React. It provides:
  • Declarative animations - Easy to read and maintain
  • Spring physics - Natural, organic movement
  • Gesture support - Drag, tap, hover, etc.
  • Layout animations - Automatic smooth transitions
  • Server-side rendering - Works with Next.js
  • TypeScript support - Full type safety

Installation

Framer Motion is already installed in this project:
"dependencies": {
  "framer-motion": "^12.23.22"
}

Core Concepts

1. Motion Components

Replace any HTML element with motion.[element] to make it animatable:
import { motion } from 'framer-motion';
 
// Before
<div className="box">Hello</div>
 
// After
<motion.div className="box">Hello</motion.div>

2. Animation Props

Basic Animation

<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.5 }}
>
  Fades in and slides up
</motion.div>

Hover Animations

<motion.button whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }}>
  Click me
</motion.button>

Exit Animations

import { AnimatePresence } from "framer-motion";
 
<AnimatePresence>
  {isVisible && (
    <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
      Content
    </motion.div>
  )}
</AnimatePresence>;

Advanced Techniques

Spring Physics

The secret to natural, smooth animations:
import { motion, useSpring } from "framer-motion";
 
const x = useMotionValue(0);
const smoothX = useSpring(x, {
  stiffness: 300, // How responsive (higher = faster)
  damping: 30, // How smooth (higher = less bounce)
  mass: 0.5, // Weight (lower = lighter feel)
});
 
<motion.div style={{ x: smoothX }}>Smooth movement</motion.div>;
Spring Configuration Guide:
  • Stiffness (100-1000): How quickly it responds
    • Low (100-200): Slow, gentle
    • Medium (300-500): Balanced
    • High (600-1000): Snappy, responsive
  • Damping (10-100): How much it bounces
    • Low (10-20): Bouncy, elastic
    • Medium (30-50): Smooth, controlled
    • High (60-100): Overdamped, no bounce
  • Mass (0.1-2): How heavy it feels
    • Low (0.1-0.5): Light, quick
    • Medium (0.5-1): Natural
    • High (1-2): Heavy, inertial

Motion Values

Direct access to animation values without re-renders:
import { motion, useMotionValue, useTransform } from "framer-motion";
 
const x = useMotionValue(0);
const opacity = useTransform(x, [-100, 0, 100], [0, 1, 0]);
 
<motion.div style={{ x, opacity }}>Scroll-linked opacity</motion.div>;

Scroll-Based Animations

import { motion, useScroll, useTransform } from "framer-motion";
 
const { scrollYProgress } = useScroll();
const scale = useTransform(scrollYProgress, [0, 1], [0.8, 1.2]);
 
<motion.div style={{ scale }}>Scales with scroll</motion.div>;

Real-World Example: Horizontal Scroll

Our client showcase uses Framer Motion for pixel-perfect horizontal scrolling:
import { motion, useMotionValue, useSpring } from "framer-motion";
 
export function ClientShowcase() {
  // Motion value for position
  const x = useMotionValue(0);
 
  // Spring physics for smooth movement
  const smoothX = useSpring(x, {
    stiffness: 300,
    damping: 30,
    mass: 0.5,
  });
 
  // Handle scroll events
  const handleWheel = (e: WheelEvent) => {
    e.preventDefault();
 
    // Update position with micro-movements
    const progress = (accumulatedDelta / threshold) * 100;
    x.set(-currentSlide * 100 - progress);
  };
 
  return (
    <motion.div className="flex h-full" style={{ x: smoothX }}>
      {slides.map((slide) => (
        <div key={slide.id}>{slide.content}</div>
      ))}
    </motion.div>
  );
}
Key Features:
  • Pixel-perfect tracking - Every scroll pixel updates position
  • Spring physics - Smooth deceleration
  • No layout shift - Pure transform animations
  • GPU accelerated - Hardware optimized

Best Practices

1. Use transform Properties

Avoid (causes layout recalculation):
animate={{ width: 200, height: 200 }}
Prefer (GPU accelerated):
animate={{ scale: 1.5 }}
GPU-accelerated properties:
  • x, y, z
  • scale, scaleX, scaleY
  • rotate, rotateX, rotateY, rotateZ
  • opacity

2. Use Motion Values for Frequent Updates

Causes re-renders:
const [x, setX] = useState(0);
<motion.div style={{ x }} />;
No re-renders:
const x = useMotionValue(0);
<motion.div style={{ x }} />;

3. Optimize with will-change

For frequently animated elements:
.animated-element {
  will-change: transform;
}

4. Use AnimatePresence for Lists

<AnimatePresence mode="popLayout">
  {items.map((item) => (
    <motion.div
      key={item.id}
      layout
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
    >
      {item.content}
    </motion.div>
  ))}
</AnimatePresence>

Common Patterns

Page Transitions

// app/layout.tsx
import { AnimatePresence } from "framer-motion";
 
export default function Layout({ children }) {
  return <AnimatePresence mode="wait">{children}</AnimatePresence>;
}
 
// app/page.tsx
import { motion } from "framer-motion";
 
export default function Page() {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      exit={{ opacity: 0, y: -20 }}
      transition={{ duration: 0.3 }}
    >
      Page content
    </motion.div>
  );
}

Stagger Animations

const container = {
  hidden: { opacity: 0 },
  show: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1,
    },
  },
};
 
const item = {
  hidden: { opacity: 0, y: 20 },
  show: { opacity: 1, y: 0 },
};
 
<motion.ul variants={container} initial="hidden" animate="show">
  {items.map((item) => (
    <motion.li key={item.id} variants={item}>
      {item.text}
    </motion.li>
  ))}
</motion.ul>;

Parallax Scrolling

import { motion, useScroll, useTransform } from "framer-motion";
 
const { scrollY } = useScroll();
const y = useTransform(scrollY, [0, 500], [0, 150]);
 
<motion.div style={{ y }}>Parallax element</motion.div>;

Smooth Hover Cards

<motion.div
  className="card"
  whileHover={{
    y: -8,
    boxShadow: "0 20px 40px rgba(0,0,0,0.2)",
  }}
  transition={{
    type: "spring",
    stiffness: 400,
    damping: 25,
  }}
>
  Card content
</motion.div>

Performance Tips

1. Use layout Prop Sparingly

Layout animations are powerful but expensive. Use only when needed:
// Good for position changes
<motion.div layout>
  Content
</motion.div>
 
// Better for most cases
<motion.div
  animate={{ x: 100, y: 100 }}
>
  Content
</motion.div>

2. Reduce Motion for Accessibility

Respect user preferences:
import { useReducedMotion } from "framer-motion";
 
const shouldReduceMotion = useReducedMotion();
 
<motion.div
  animate={{
    scale: shouldReduceMotion ? 1 : 1.2,
    transition: { duration: shouldReduceMotion ? 0 : 0.3 },
  }}
>
  Content
</motion.div>;

3. Lazy Load Heavy Animations

import { lazy, Suspense } from "react";
 
const HeavyAnimation = lazy(() => import("./HeavyAnimation"));
 
<Suspense fallback={<div>Loading...</div>}>
  <HeavyAnimation />
</Suspense>;

Debugging

Visualize Animation Values

import { motion, useMotionValue } from "framer-motion";
import { useEffect } from "react";
 
const x = useMotionValue(0);
 
useEffect(() => {
  const unsubscribe = x.on("change", (latest) => {
    console.log("x:", latest);
  });
  return unsubscribe;
}, [x]);

Check for Performance Issues

// Add this to see when components re-render
useEffect(() => {
  console.log("Component rendered");
});

Resources

Official Documentation

Examples in This Project

  • src/components/layout/sections/landing/client-showcase.tsx - Horizontal scroll with spring physics
  • More examples coming soon!

Community Resources

Troubleshooting

Animation Not Working

  1. Check if component is wrapped in motion:
    // ❌ Won't animate
    <div animate={{ x: 100 }} />
     
    // ✅ Will animate
    <motion.div animate={{ x: 100 }} />
  2. Verify initial values:
    // ❌ No visible change
    <motion.div animate={{ opacity: 1 }} />
     
    // ✅ Visible change
    <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />

Janky Animations

  1. Use transform properties (x, y, scale) instead of layout properties (width, height, top, left)
  2. Add will-change: transform CSS
  3. Reduce complexity - fewer animated elements perform better
  4. Check for forced reflows - avoid reading layout properties during animation

Exit Animations Not Playing

Always wrap with AnimatePresence:
import { AnimatePresence } from "framer-motion";
 
<AnimatePresence>{show && <motion.div exit={{ opacity: 0 }}>Content</motion.div>}</AnimatePresence>;

Next Steps

  1. Experiment with spring physics in client-showcase.tsx
  2. Try adding hover animations to cards
  3. Implement page transitions
  4. Create custom animation variants
For questions or help, check the official documentation or ask the team!