April 8, 2026/Intermediate/Scroll & Parallax

Parallax Scroll

A section with large headings, letter-by-letter random parallax offsets, and three images drifting upward at different speeds (0, -150px, -250px) as you scroll. Uses Framer Motion useScroll and useTransform.

Parallax

Scroll

smooth motion

"use client";
import { useRef, useMemo } from "react";
import Image from "next/image";
import { motion, useScroll, useTransform } from "framer-motion";
import styles from "./styles.module.css";

// Each letter needs its own useTransform — extracted to avoid hooks-in-map
function Letter({ char, progress, yOffset }) {
  const y = useTransform(progress, [0, 1], [0, yOffset]);
  return (
    <motion.span className={styles.letter} style={{ top: y }}>
      {char === " " ? "\u00A0" : char}
    </motion.span>
  );
}

// Each image needs its own y transform
function ParallaxImage({ src, progress, yRange }) {
  const y = useTransform(progress, [0, 1], yRange);
  return (
    <motion.div className={styles.imageContainer} style={{ y }}>
      <Image src={src} alt="" fill className={styles.img} sizes="28vw" />
    </motion.div>
  );
}

export default function ParallaxScroll({ word = "smooth motion", images = [] }) {
  const containerRef = useRef(null);

  const { scrollYProgress } = useScroll({
    target: containerRef,
    offset: ["start end", "end start"],
  });

  // Pre-compute stable random offsets — computed once, not on every render
  const letterOffsets = useMemo(
    () => word.split("").map(() => Math.floor(Math.random() * -75) - 25),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [word]
  );

  // Three speed tiers for images
  const Y_RANGES = [[0, -50], [0, -150], [0, -250]];

  return (
    <div ref={containerRef} className={styles.container}>
      {/* ── Headings ── */}
      <div className={styles.body}>
        <motion.h1
          className={styles.heading}
          style={{ y: useTransform(scrollYProgress, [0, 1], [0, -50]) }}
        >
          Parallax
        </motion.h1>
        <h1 className={styles.heading}>Scroll</h1>

        {/* ── Letter-by-letter random parallax ── */}
        <div className={styles.word}>
          <p>
            {word.split("").map((char, i) => (
              <Letter
                key={i}
                char={char}
                progress={scrollYProgress}
                yOffset={letterOffsets[i]}
              />
            ))}
          </p>
        </div>
      </div>

      {/* ── Images at three different parallax speeds ── */}
      <div className={styles.images}>
        {images.slice(0, 3).map((src, i) => (
          <ParallaxImage
            key={i}
            src={src}
            progress={scrollYProgress}
            yRange={Y_RANGES[i] ?? [0, -50]}
          />
        ))}
      </div>
    </div>
  );
}
  • framer-motion

Related Components

NEWTODAY
Exo Ape Play Reelscroll-parallax
NEW5D AGO
Garoa Video Scrollscroll-parallax
NEW5D AGO
Relai Parallax Scroll Heroscroll-parallax