JB logo

Command Palette

Search for a command to run...

yOUTUBE
Blog
Next

Framer Motion Scroll Animations Guide - Next.js, TypeScript & Tailwind

Master scroll animations with Framer Motion in Next.js. Learn whileInView, useInView, and useScroll hooks through 4 production-ready case studies - fade/zoom sections, stacked cards, horizontal parallax galleries, and text reveal effects with complete TypeScript examples.

Framer Motion Scroll Animations Documentation

Complete Guide for Next.js with TypeScript and Tailwind CSS


Table of Contents

  1. Introduction
  2. Setup and Installation
  3. Core Concepts
  4. Case Study 1: Fade & Zoom Sections on Scroll
  5. Case Study 2: Stacked Sections
  6. Case Study 3: Horizontal Scroll Gallery with Parallax
  7. Case Study 4: Text Reveal with Progress Indicator
  8. Advanced Tips and Best Practices
  9. Performance Optimization
  10. Troubleshooting

Introduction

Framer Motion is a powerful animation library for React that makes creating scroll-based animations intuitive and performant. This documentation provides practical, production-ready examples for common scroll animation patterns in modern web applications.

What You'll Learn

  • Three distinct methods for scroll animations: whileInView, useInView, and useScroll
  • When to use each method for optimal results
  • Real-world implementations with complete code examples
  • Performance optimization techniques

Setup and Installation

Prerequisites

  • Node.js 16.x or higher
  • Next.js 13.x or higher
  • Basic understanding of React and TypeScript

Step 1: Create a Next.js Project

pnpm create next-app@latest framer-scroll-demo --typescript --tailwind --app
cd framer-scroll-demo

Step 2: Install Framer Motion

pnpm add framer-motion

Step 3: Project Structure

framer-scroll-demo/
├── app/
│   ├── page.tsx                 # Main page
│   ├── layout.tsx              # Root layout
│   └── globals.css             # Global styles
├── components/
│   ├── FadeZoomSections.tsx    # Case Study 1
│   ├── StackedSections.tsx     # Case Study 2
│   ├── HorizontalGallery.tsx   # Case Study 3
│   └── TextReveal.tsx          # Case Study 4
└── public/
    └── images/                  # Image assets

Core Concepts

Three Animation Approaches

1. whileInView (Simplest)

  • Best for: Basic fade-ins, simple animations
  • Usage: Add directly to motion components
  • Limitation: One animation per element
<motion.div
  initial={{ opacity: 0 }}
  whileInView={{ opacity: 1 }}
  viewport={{ once: true, amount: 0.5 }}
/>

2. useInView (Boolean-based)

  • Best for: Multiple synchronized animations
  • Usage: Returns boolean indicating viewport status
  • Use case: Orchestrating several elements
const ref = useRef(null);
const isInView = useInView(ref, { amount: 0.5 });

3. useScroll (Continuous)

  • Best for: Progress-based animations, parallax
  • Usage: Returns scroll progress values (0-1)
  • Use case: Smooth, continuous effects
const { scrollYProgress } = useScroll({
  target: ref,
  offset: ["start end", "end start"],
});

Case Study 1: Fade & Zoom Sections on Scroll

Overview

Create a landing page with 5 sections that fade in and zoom in from different directions as they enter the viewport.

Features

  • Alternating animation directions
  • Smooth fade and scale transitions
  • Responsive design
  • Configurable animation delays

Step-by-Step Implementation

Step 1: Create the Component File

Create components/FadeZoomSections.tsx:

"use client";
 
import { motion } from "framer-motion";
import { useRef } from "react";
 
interface SectionProps {
  title: string;
  description: string;
  color: string;
  direction: "left" | "right" | "up" | "down";
}
 
const Section = ({ title, description, color, direction }: SectionProps) => {
  // Define animation variants based on direction
  const variants = {
    hidden: {
      opacity: 0,
      scale: 0.8,
      x: direction === "left" ? -100 : direction === "right" ? 100 : 0,
      y: direction === "up" ? -100 : direction === "down" ? 100 : 0,
    },
    visible: {
      opacity: 1,
      scale: 1,
      x: 0,
      y: 0,
      transition: {
        duration: 0.8,
        ease: [0.17, 0.55, 0.55, 1], // Custom easing
      },
    },
  };
 
  return (
    <motion.section
      className={`flex min-h-screen items-center justify-center ${color}`}
      initial="hidden"
      whileInView="visible"
      viewport={{ once: true, amount: 0.3 }}
      variants={variants}
    >
      <div className="mx-auto max-w-4xl px-6 text-center">
        <motion.h2
          className="mb-6 text-5xl font-bold md:text-7xl"
          variants={{
            hidden: { opacity: 0, y: 20 },
            visible: {
              opacity: 1,
              y: 0,
              transition: { delay: 0.2, duration: 0.6 },
            },
          }}
        >
          {title}
        </motion.h2>
        <motion.p
          className="text-xl opacity-90 md:text-2xl"
          variants={{
            hidden: { opacity: 0, y: 20 },
            visible: {
              opacity: 1,
              y: 0,
              transition: { delay: 0.4, duration: 0.6 },
            },
          }}
        >
          {description}
        </motion.p>
      </div>
    </motion.section>
  );
};
 
export default function FadeZoomSections() {
  const sections: SectionProps[] = [
    {
      title: "Welcome",
      description: "Experience smooth scroll animations with Framer Motion",
      color: "bg-gradient-to-br from-purple-600 to-blue-600 text-white",
      direction: "up",
    },
    {
      title: "Our Services",
      description: "Building modern web experiences that captivate users",
      color: "bg-gradient-to-br from-blue-600 to-cyan-600 text-white",
      direction: "left",
    },
    {
      title: "Featured Work",
      description: "Showcasing projects that push the boundaries of design",
      color: "bg-gradient-to-br from-cyan-600 to-teal-600 text-white",
      direction: "right",
    },
    {
      title: "Our Team",
      description: "Meet the creative minds behind innovative solutions",
      color: "bg-gradient-to-br from-teal-600 to-green-600 text-white",
      direction: "left",
    },
    {
      title: "Get Started",
      description: "Ready to transform your digital presence?",
      color: "bg-gradient-to-br from-green-600 to-emerald-600 text-white",
      direction: "down",
    },
  ];
 
  return (
    <div className="overflow-x-hidden">
      {sections.map((section, index) => (
        <Section key={index} {...section} />
      ))}
    </div>
  );
}

Step 2: Add to Your Page

Update app/page.tsx:

import FadeZoomSections from "@/components/FadeZoomSections";
 
export default function Home() {
  return (
    <main>
      <FadeZoomSections />
    </main>
  );
}

Step 3: Customize Colors and Animations

Modify the sections array to change colors, text, or animation directions:

// Change direction for different effects
direction: "left"; // Slides in from left
direction: "right"; // Slides in from right
direction: "up"; // Zooms in from top
direction: "down"; // Zooms in from bottom

Key Concepts Explained

  1. Viewport Configuration

    viewport={{ once: true, amount: 0.3 }}
    • once: true - Animation triggers only once
    • amount: 0.3 - Triggers when 30% of element is visible
  2. Staggered Children

    variants={{
      hidden: { opacity: 0, y: 20 },
      visible: {
        opacity: 1,
        y: 0,
        transition: { delay: 0.4 }  // Stagger effect
      }
    }}
  3. Custom Easing

    ease: [0.17, 0.55, 0.55, 1]; // Cubic bezier curve

Case Study 2: Stacked Sections

Overview

Create sections that stack on top of each other as you scroll, creating a layered card effect popular on modern websites.

Features

  • Smooth stacking animation
  • Scale and opacity transitions
  • Sticky positioning
  • Z-index management

Step-by-Step Implementation

Step 1: Create the Component

Create components/StackedSections.tsx:

"use client";
 
import { motion, useScroll, useTransform } from "framer-motion";
import { useRef } from "react";
 
interface StackedCardProps {
  index: number;
  title: string;
  description: string;
  color: string;
  progress: any;
  range: [number, number];
  targetScale: number;
}
 
const StackedCard = ({
  index,
  title,
  description,
  color,
  progress,
  range,
  targetScale,
}: StackedCardProps) => {
  const container = useRef(null);
 
  // Transform scroll progress to scale
  const scale = useTransform(progress, range, [1, targetScale]);
 
  return (
    <div
      ref={container}
      className="sticky top-0 flex h-screen items-center justify-center"
      style={{
        top: `calc(${index * 50}px)`,
      }}
    >
      <motion.div
        style={{
          scale,
          backgroundColor: color,
        }}
        className="relative flex h-[600px] w-full max-w-5xl origin-top flex-col justify-between rounded-3xl p-12 shadow-2xl"
      >
        <div>
          <h2 className="mb-6 text-6xl font-bold text-white">{title}</h2>
          <p className="text-2xl text-white/90">{description}</p>
        </div>
 
        <div className="flex items-end justify-between">
          <div className="text-lg text-white/70">Section {index + 1}</div>
          <div className="text-sm text-white/50">Scroll to continue</div>
        </div>
      </motion.div>
    </div>
  );
};
 
export default function StackedSections() {
  const container = useRef(null);
 
  const { scrollYProgress } = useScroll({
    target: container,
    offset: ["start start", "end end"],
  });
 
  const sections = [
    {
      title: "Innovation",
      description:
        "Pioneering the future of digital experiences through cutting-edge technology and creative thinking.",
      color: "#1e40af", // blue-800
    },
    {
      title: "Design",
      description:
        "Crafting beautiful, intuitive interfaces that users love to interact with every single day.",
      color: "#7c3aed", // violet-600
    },
    {
      title: "Development",
      description:
        "Building robust, scalable solutions that stand the test of time and user demands.",
      color: "#db2777", // pink-600
    },
  ];
 
  return (
    <div ref={container} className="relative bg-black">
      {/* Hero Section */}
      <div className="flex h-screen items-center justify-center bg-gradient-to-b from-gray-900 to-black">
        <div className="text-center">
          <motion.h1
            className="mb-6 text-7xl font-bold text-white md:text-9xl"
            initial={{ opacity: 0, y: 50 }}
            animate={{ opacity: 1, y: 0 }}
            transition={{ duration: 0.8 }}
          >
            Stacked Cards
          </motion.h1>
          <motion.p
            className="text-xl text-gray-400 md:text-2xl"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            transition={{ delay: 0.3, duration: 0.8 }}
          >
            Scroll to see the magic unfold
          </motion.p>
        </div>
      </div>
 
      {/* Stacked Cards */}
      {sections.map((section, index) => {
        const targetScale = 1 - (sections.length - index) * 0.05;
        const range: [number, number] = [index / sections.length, 1];
 
        return (
          <StackedCard
            key={index}
            index={index}
            {...section}
            progress={scrollYProgress}
            range={range}
            targetScale={targetScale}
          />
        );
      })}
 
      {/* Footer Spacing */}
      <div className="flex h-screen items-center justify-center bg-black">
        <p className="text-xl text-white/50">End of stacked sections</p>
      </div>
    </div>
  );
}

Step 2: Understanding the Stacking Logic

Key Concepts:

  1. Sticky Positioning

    style={{ top: `calc(${index * 50}px)` }}

    Each card sticks at a slight offset, creating the stacked effect.

  2. Scale Transformation

    const targetScale = 1 - (sections.length - index) * 0.05;

    Cards beneath scale down slightly as new cards appear.

  3. Scroll Range Mapping

    const range: [number, number] = [index / sections.length, 1];

    Defines when each card's animation starts and ends.

Step 3: Customization Options

// Adjust stacking offset
style={{ top: `calc(${index * 50}px)` }}  // Change 50 to adjust spacing
 
// Modify scale intensity
const targetScale = 1 - ((sections.length - index) * 0.05);  // Change 0.05
 
// Change card height
className="... h-[600px] ..."  // Adjust height value

Visual Flow Explanation

Scroll Position: 0%
┌─────────────┐
│  Section 1  │ scale: 1.0
└─────────────┘

Scroll Position: 33%
┌─────────────┐
│  Section 2  │ scale: 1.0
├─────────────┤
│  Section 1  │ scale: 0.95
└─────────────┘

Scroll Position: 66%
┌─────────────┐
│  Section 3  │ scale: 1.0
├─────────────┤
│  Section 2  │ scale: 0.95
├─────────────┤
│  Section 1  │ scale: 0.90
└─────────────┘

Overview

Create a horizontal scrolling gallery where images move at different speeds, creating a parallax effect. This is commonly seen on portfolio websites and product showcases.

Features

  • Horizontal scroll with vertical scrolling
  • Multi-layer parallax effect
  • Smooth image transitions
  • Responsive grid layout

Step-by-Step Implementation

Create components/HorizontalGallery.tsx:

"use client";
 
import { motion, useScroll, useTransform } from "framer-motion";
import { useRef } from "react";
import Image from "next/image";
 
interface GalleryItem {
  id: number;
  title: string;
  image: string;
  speed: number; // Parallax speed multiplier
}
 
export default function HorizontalGallery() {
  const container = useRef(null);
 
  const { scrollYProgress } = useScroll({
    target: container,
    offset: ["start end", "end start"],
  });
 
  const items: GalleryItem[] = [
    {
      id: 1,
      title: "Mountain Vista",
      image: "/images/mountain.jpg",
      speed: 0.5,
    },
    { id: 2, title: "Urban Landscape", image: "/images/city.jpg", speed: 1.5 },
    { id: 3, title: "Ocean Sunset", image: "/images/ocean.jpg", speed: 0.8 },
    { id: 4, title: "Forest Path", image: "/images/forest.jpg", speed: 1.2 },
    { id: 5, title: "Desert Dunes", image: "/images/desert.jpg", speed: 0.6 },
    {
      id: 6,
      title: "Northern Lights",
      image: "/images/aurora.jpg",
      speed: 1.4,
    },
  ];
 
  return (
    <div className="bg-black text-white">
      {/* Hero Section */}
      <div className="flex h-screen items-center justify-center">
        <div className="text-center">
          <motion.h1
            className="mb-6 text-7xl font-bold md:text-9xl"
            initial={{ opacity: 0, scale: 0.8 }}
            animate={{ opacity: 1, scale: 1 }}
            transition={{ duration: 1 }}
          >
            Gallery
          </motion.h1>
          <motion.p
            className="text-xl text-gray-400"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            transition={{ delay: 0.3 }}
          >
            Scroll to explore our work
          </motion.p>
        </div>
      </div>
 
      {/* Horizontal Scroll Container */}
      <div ref={container} className="relative h-[300vh]">
        <div className="sticky top-0 flex h-screen items-center overflow-hidden">
          <div className="flex gap-8 px-8">
            {items.map((item, index) => (
              <GalleryCard
                key={item.id}
                item={item}
                progress={scrollYProgress}
                index={index}
                total={items.length}
              />
            ))}
          </div>
        </div>
      </div>
 
      {/* Footer */}
      <div className="flex h-screen items-center justify-center">
        <p className="text-xl text-gray-500">End of gallery</p>
      </div>
    </div>
  );
}
 
interface GalleryCardProps {
  item: GalleryItem;
  progress: any;
  index: number;
  total: number;
}
 
const GalleryCard = ({ item, progress, index, total }: GalleryCardProps) => {
  // Calculate horizontal movement based on scroll progress
  // Each card moves at a different speed based on item.speed
  const baseX = useTransform(progress, [0, 1], [0, -1000 * item.speed]);
 
  // Add slight rotation for dynamic effect
  const rotate = useTransform(
    progress,
    [0, 0.5, 1],
    [0, index % 2 === 0 ? 2 : -2, 0]
  );
 
  // Scale effect as cards enter/exit view
  const scale = useTransform(
    progress,
    [(index - 1) / total, index / total, (index + 1) / total],
    [0.8, 1, 0.8]
  );
 
  return (
    <motion.div
      style={{
        x: baseX,
        rotate,
        scale,
      }}
      className="group relative h-[600px] w-[500px] flex-shrink-0 overflow-hidden rounded-2xl"
    >
      {/* Image with hover effect */}
      <div className="relative h-full w-full bg-gray-900">
        <div className="absolute inset-0 z-10 bg-gradient-to-t from-black/80 via-black/20 to-transparent" />
 
        {/* Placeholder for actual images */}
        <div
          className="h-full w-full bg-gradient-to-br from-purple-600 to-blue-600"
          style={{
            backgroundImage: `linear-gradient(135deg, 
              hsl(${index * 60}, 70%, 50%), 
              hsl(${index * 60 + 60}, 70%, 40%))`,
          }}
        />
 
        {/* Title overlay */}
        <motion.div
          className="absolute right-0 bottom-0 left-0 z-20 p-8"
          initial={{ y: 20, opacity: 0 }}
          whileInView={{ y: 0, opacity: 1 }}
          transition={{ delay: index * 0.1 }}
        >
          <h3 className="mb-2 text-4xl font-bold">{item.title}</h3>
          <p className="text-gray-300">Project {item.id}</p>
        </motion.div>
      </div>
 
      {/* Hover overlay */}
      <motion.div
        className="absolute inset-0 flex items-center justify-center bg-white/10 opacity-0 backdrop-blur-sm transition-opacity duration-300 group-hover:opacity-100"
        initial={{ opacity: 0 }}
      >
        <button className="rounded-full bg-white px-8 py-4 text-lg font-semibold text-black transition-transform hover:scale-110">
          View Project
        </button>
      </motion.div>
    </motion.div>
  );
};

Step 2: Understanding Horizontal Scrolling

Container Structure:

<div className="h-[300vh]">
  {" "}
  {/* Tall container for scroll distance */}
  <div className="sticky top-0 h-screen">
    {" "}
    {/* Sticky viewport */}
    <div className="flex gap-8">
      {" "}
      {/* Horizontal items */}
      {/* Cards here */}
    </div>
  </div>
</div>

Parallax Calculation:

const baseX = useTransform(
  progress, // Input: 0 to 1
  [0, 1], // Input range
  [0, -1000 * item.speed] // Output: horizontal position
);

Step 3: Advanced Parallax Techniques

// Different parallax layers
const backgroundX = useTransform(progress, [0, 1], [0, -500]); // Slow
const middlegroundX = useTransform(progress, [0, 1], [0, -1000]); // Medium
const foregroundX = useTransform(progress, [0, 1], [0, -1500]); // Fast

Step 4: Adding Real Images

Replace the placeholder div with Next.js Image component:

<Image
  src={item.image}
  alt={item.title}
  fill
  className="object-cover"
  sizes="(max-width: 768px) 100vw, 500px"
/>

Customization Guide

// Adjust scroll distance
<div className="h-[300vh]">  // Change 300vh for longer/shorter scroll
 
// Modify parallax intensity
speed: 0.5   // Slower movement
speed: 1.0   // Normal speed
speed: 2.0   // Faster movement
 
// Change card dimensions
className="w-[500px] h-[600px]"  // Adjust size
 
// Adjust gap between cards
className="flex gap-8"  // Change gap-8 to desired spacing

Case Study 4: Text Reveal with Progress Indicator

Overview

Create a text reveal animation with a reading progress indicator, perfect for blog posts, articles, and storytelling pages.

Features

  • Character-by-character text reveal
  • Scroll progress bar
  • Smooth color transitions
  • Sticky progress indicator

Step-by-Step Implementation

Step 1: Create the Component

Create components/TextReveal.tsx:

"use client";
 
import { motion, useScroll, useTransform } from "framer-motion";
import { useRef } from "react";
 
export default function TextReveal() {
  const container = useRef(null);
 
  const { scrollYProgress } = useScroll({
    target: container,
    offset: ["start start", "end end"],
  });
 
  // Progress bar width
  const progressWidth = useTransform(scrollYProgress, [0, 1], ["0%", "100%"]);
 
  // Background color transition
  const backgroundColor = useTransform(
    scrollYProgress,
    [0, 0.5, 1],
    ["#0f172a", "#1e1b4b", "#312e81"] // slate-900 to indigo-900
  );
 
  const paragraphs = [
    "In the realm of modern web design, scroll animations have emerged as a powerful tool for creating engaging user experiences. They transform static content into dynamic narratives that unfold as users navigate through your site.",
 
    "Framer Motion provides an elegant API for implementing these animations with minimal code. Its declarative approach means you describe what you want, not how to achieve it, leading to cleaner and more maintainable codebases.",
 
    "The key to effective scroll animations lies in subtlety and purpose. Every animation should serve the content, guiding users' attention and making the experience more intuitive rather than distracting from the message.",
 
    "Performance is crucial when implementing scroll animations. Framer Motion runs animations off the main thread when possible, ensuring smooth experiences even on lower-end devices. Always test your animations across different devices and browsers.",
 
    "As we push the boundaries of what's possible on the web, remember that accessibility should never be compromised. Provide alternatives for users who prefer reduced motion, and ensure your content remains accessible regardless of animation state.",
  ];
 
  return (
    <div className="relative">
      {/* Fixed Progress Bar */}
      <div className="fixed top-0 right-0 left-0 z-50 h-2 bg-gray-800">
        <motion.div
          className="h-full bg-gradient-to-r from-purple-500 via-pink-500 to-red-500"
          style={{ width: progressWidth }}
        />
      </div>
 
      {/* Hero Section */}
      <motion.div
        className="flex min-h-screen items-center justify-center"
        style={{ backgroundColor }}
      >
        <div className="mx-auto max-w-4xl px-6 text-center">
          <motion.h1
            className="mb-6 text-7xl font-bold text-white md:text-9xl"
            initial={{ opacity: 0, y: 100 }}
            animate={{ opacity: 1, y: 0 }}
            transition={{ duration: 1, ease: [0.17, 0.55, 0.55, 1] }}
          >
            Read & Reveal
          </motion.h1>
          <motion.p
            className="text-2xl text-white/70"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            transition={{ delay: 0.5, duration: 0.8 }}
          >
            Watch the story unfold as you scroll
          </motion.p>
        </div>
      </motion.div>
 
      {/* Text Reveal Sections */}
      <div ref={container} className="relative">
        {paragraphs.map((paragraph, index) => (
          <RevealParagraph key={index} text={paragraph} index={index} />
        ))}
      </div>
 
      {/* Footer */}
      <motion.div
        className="flex min-h-screen items-center justify-center bg-black"
        initial={{ opacity: 0 }}
        whileInView={{ opacity: 1 }}
        transition={{ duration: 1 }}
      >
        <div className="text-center">
          <h2 className="mb-4 text-5xl font-bold text-white">The End</h2>
          <p className="text-xl text-gray-400">Thanks for scrolling</p>
        </div>
      </motion.div>
    </div>
  );
}
 
interface RevealParagraphProps {
  text: string;
  index: number;
}
 
const RevealParagraph = ({ text, index }: RevealParagraphProps) => {
  const ref = useRef(null);
 
  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ["start 0.8", "start 0.3"],
  });
 
  // Split text into words for animation
  const words = text.split(" ");
 
  return (
    <div
      ref={ref}
      className="flex min-h-screen items-center justify-center px-6"
    >
      <div className="max-w-4xl">
        <motion.p
          className="text-4xl leading-relaxed md:text-5xl"
          initial={{ opacity: 0 }}
          whileInView={{ opacity: 1 }}
          transition={{ duration: 0.8 }}
        >
          {words.map((word, wordIndex) => (
            <motion.span
              key={wordIndex}
              className="mr-3 mb-2 inline-block"
              style={{
                opacity: useTransform(
                  scrollYProgress,
                  [wordIndex / words.length, (wordIndex + 1) / words.length],
                  [0.3, 1]
                ),
              }}
            >
              {word}
            </motion.span>
          ))}
        </motion.p>
 
        {/* Section number indicator */}
        <motion.div
          className="mt-12 font-mono text-xl text-white/30"
          initial={{ opacity: 0, y: 20 }}
          whileInView={{ opacity: 1, y: 0 }}
          transition={{ delay: 0.5 }}
        >
          {String(index + 1).padStart(2, "0")}
        </motion.div>
      </div>
    </div>
  );
};

Step 2: Understanding Text Reveal

Word-by-Word Animation:

const words = text.split(" ");
 
{
  words.map((word, wordIndex) => (
    <motion.span
      style={{
        opacity: useTransform(
          scrollYProgress,
          [wordIndex / words.length, (wordIndex + 1) / words.length],
          [0.3, 1]
        ),
      }}
    >
      {word}
    </motion.span>
  ));
}

Progress Mapping:

  • Each word occupies a portion of scroll progress
  • Word 1: 0-10% → opacity 0.3-1
  • Word 2: 10-20% → opacity 0.3-1
  • Word 3: 20-30% → opacity 0.3-1

Step 3: Alternative Reveal Styles

Character-by-Character:

const characters = text.split("");
 
{
  characters.map((char, i) => (
    <motion.span
      style={{
        opacity: useTransform(
          scrollYProgress,
          [i / characters.length, (i + 1) / characters.length],
          [0, 1]
        ),
      }}
    >
      {char}
    </motion.span>
  ));
}

Line-by-Line:

const lines = text.split("\n");
 
{
  lines.map((line, i) => (
    <motion.div
      initial={{ opacity: 0, x: -50 }}
      whileInView={{ opacity: 1, x: 0 }}
      transition={{ delay: i * 0.2 }}
    >
      {line}
    </motion.div>
  ));
}

Customization Options

// Adjust reveal speed
offset: ['start 0.8', 'start 0.3']  // Faster reveal
offset: ['start 0.9', 'start 0.5']  // Slower reveal
 
// Change text opacity range
[0.3, 1]  // Slight fade to full opacity
[0, 1]    // Complete fade in
[0.5, 1]  // Minimal fade effect
 
// Modify progress bar colors
from-purple-500 via-pink-500 to-red-500  // Gradient
bg-blue-500  // Solid color

Advanced Tips and Best Practices

1. Viewport Configuration

// Trigger when fully visible
viewport={{ once: true, amount: 1 }}
 
// Trigger when 50% visible (good default)
viewport={{ once: false, amount: 0.5 }}
 
// Trigger immediately when entering
viewport={{ once: true, amount: 0 }}
 
// Trigger with margin offset
viewport={{
  once: true,
  margin: "-100px"  // Trigger 100px before entering
}}

2. Scroll Offset Patterns

// Start when element enters, end when it exits
offset: ["start end", "end start"];
 
// Start when element center hits viewport center
offset: ["center center", "center center"];
 
// Start at top, end when element exits top
offset: ["start start", "end start"];
 
// Custom percentages
offset: ["start 80%", "end 20%"];

3. Performance Optimization

// Use transform instead of position properties
// GOOD: transform: translateX()
// BAD: left: X
 
// Debounce scroll listeners
const { scrollY } = useScroll();
const throttledScroll = useTransform(scrollY, (latest) =>
  Math.round(latest / 10) * 10  // Updates every 10px
);
 
// Minimize re-renders
// Use motion values directly in style
style={{ x: scrollXProgress }}
// Instead of
style={{ x: useTransform(scrollXProgress, v => v) }}

4. Responsive Animations

// Disable on mobile
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768;
 
<motion.div
  initial={isMobile ? {} : { opacity: 0 }}
  animate={isMobile ? {} : { opacity: 1 }}
/>
 
// Adjust animation values for screen size
const xOffset = typeof window !== 'undefined' && window.innerWidth < 768
  ? 50 : 100;
 
initial={{ x: -xOffset }}

5. Accessibility Considerations

// Respect prefers-reduced-motion
"use client";
 
import { useReducedMotion } from "framer-motion";
 
function MyComponent() {
  const shouldReduceMotion = useReducedMotion();
 
  return (
    <motion.div
      initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 50 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{
        duration: shouldReduceMotion ? 0 : 0.5,
      }}
    />
  );
}

Performance Optimization

1. Use will-change Sparingly

// Only for animated properties
<motion.div
  style={{ willChange: 'transform' }}
  animate={{ x: 100 }}
/>
 
// Remove after animation completes
<motion.div
  animate={{ x: 100 }}
  onAnimationComplete={() => {
    // will-change auto-removed by Framer Motion
  }}
/>

2. Lazy Load Heavy Components

import dynamic from "next/dynamic";
 
const HeavyAnimation = dynamic(() => import("@/components/HeavyAnimation"), {
  ssr: false,
});

3. Optimize Images

// Use Next.js Image optimization
<Image
  src="/image.jpg"
  alt="Description"
  width={800}
  height={600}
  loading="lazy"
  placeholder="blur"
/>

4. Minimize Layout Shifts

// Reserve space for animated elements
<div className="min-h-[500px]">
  <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
    Content
  </motion.div>
</div>

Troubleshooting

Common Issues and Solutions

1. Animations Not Triggering

Problem: Animation doesn't play on scroll

Solution:

// Ensure proper viewport configuration
viewport={{ once: true, amount: 0.3 }}  // Not amount: 0
 
// Check if element is actually in viewport
whileInView={{ opacity: 1 }}
// Add onViewportEnter for debugging
onViewportEnter={() => console.log('In view!')}

2. Jerky Scrolling

Problem: Animations cause janky scroll

Solution:

// Use transform instead of position
// GOOD
animate={{ x: 100, y: 100 }}
 
// BAD
animate={{ left: 100, top: 100 }}
 
// Enable GPU acceleration
style={{
  transform: 'translateZ(0)',
  willChange: 'transform'
}}

3. SSR Errors

Problem: "window is not defined"

Solution:

// Add 'use client' directive
"use client";
 
// Or check for window
const isBrowser = typeof window !== "undefined";

4. Memory Leaks

Problem: Performance degrades over time

Solution:

// Clean up scroll listeners
useEffect(() => {
  const cleanup = someScrollListener();
  return () => cleanup();
}, []);
 
// Use once: true for one-time animations
viewport={{ once: true }}

5. Hydration Mismatch

Problem: Next.js hydration errors

Solution:

// Disable SSR for animation
import dynamic from "next/dynamic";
 
const AnimatedComponent = dynamic(() => import("./AnimatedComponent"), {
  ssr: false,
});

Additional Resources

Framer Motion Documentation

Example Repositories

  • View source code at: [GitHub Repository Link]
  • Live demos at: [Demo Site Link]

Community Resources

  • Framer Motion Discord
  • Stack Overflow tag: framer-motion
  • CodeSandbox examples

Conclusion

Scroll animations, when implemented thoughtfully, can transform a static website into an engaging, memorable experience. The four case studies covered in this documentation represent common patterns you'll encounter in modern web development:

  1. Fade & Zoom Sections - Perfect for landing pages and storytelling
  2. Stacked Sections - Ideal for highlighting sequential content
  3. Horizontal Gallery - Great for portfolios and product showcases
  4. Text Reveal - Excellent for long-form content and articles

Remember these key principles:

  • Performance first - Use transforms and opacity
  • Purposeful animations - Every animation should enhance UX
  • Accessibility - Respect reduced motion preferences
  • Progressive enhancement - Content should work without animations

Start with simple animations and gradually build complexity as you become comfortable with the API. The most effective scroll animations are often the subtlest ones.

Happy animating! 🎨✨