AnimationsSimpleApril 8, 2026
Heading Scroll Parallax
Oversized display headings drift vertically at different rates as you scroll, creating layered depth. Each heading moves independently using useTransform with unique y ranges.
View Full Demo →Preview
Motion
Design
Studio
Source
demo.jsx
import HeadingScrollParallax from "./index.jsx";
import styles from "./demo.module.css";
export default function HeadingScrollParallaxDemo() {
return (
<div>
{/* Intro — pushes the parallax section below the fold */}
<section className={styles.intro}>
<p className={styles.introLabel}>Scroll animation</p>
<h2 className={styles.introTitle}>Each heading moves at a different speed as you scroll through the section.</h2>
</section>
{/* The component — scroll into this to see the parallax */}
<HeadingScrollParallax headings={["Motion", "Design", "Studio"]} />
{/* Trailing space — lets you scroll past the section */}
<section className={styles.outro}>
<p className={styles.outroText}>Keep scrolling to see the full effect.</p>
</section>
</div>
);
}
index.jsx
"use client";
import { useRef } from "react";
import { useScroll, useTransform, motion } from "framer-motion";
import styles from "./styles.module.css";
export default function HeadingScrollParallax({ headings = [] }) {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"],
});
const y0 = useTransform(scrollYProgress, [0, 1], ["0vh", "-20vh"]);
const y1 = useTransform(scrollYProgress, [0, 1], ["0vh", "-10vh"]);
const y2 = useTransform(scrollYProgress, [0, 1], ["0vh", "-30vh"]);
const yValues = [y0, y1, y2];
return (
<section ref={ref} className={styles.section}>
<div className={styles.headings}>
{headings.map((heading, i) => (
<motion.h2
key={heading}
className={styles.heading}
style={{ y: yValues[i % 3], willChange: "transform" }}
>
{heading}
</motion.h2>
))}
</div>
</section>
);
}
demo.module.css
.intro {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 3rem;
background: #f5f5f0;
}
.introLabel {
font-size: 0.75rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: #9b9b9b;
margin-bottom: 1.25rem;
}
.introTitle {
font-size: clamp(1.5rem, 3vw, 2.5rem);
font-weight: 600;
letter-spacing: -0.03em;
color: #1a1a1a;
max-width: 640px;
line-height: 1.2;
}
.outro {
min-height: 50vh;
display: flex;
align-items: center;
justify-content: center;
background: #0d0d0d;
}
.outroText {
font-size: 0.875rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.25);
}
styles.module.css
.section {
position: relative;
min-height: 60vh;
display: flex;
align-items: center;
justify-content: center;
padding: 4rem 2rem;
background: #0d0d0d;
overflow: hidden;
}
.headings {
display: flex;
flex-direction: column;
gap: 0;
align-items: center;
}
.heading {
font-size: clamp(3rem, 10vw, 8rem);
font-weight: 700;
letter-spacing: -0.04em;
color: #ffffff;
line-height: 0.9;
margin: 0;
white-space: nowrap;
}
.heading:nth-child(even) {
color: transparent;
-webkit-text-stroke: 1px rgba(255, 255, 255, 0.3);
}
Dependencies
framer-motion