Scroll Animations
Expert knowledge for scroll-triggered animations, parallax effects, and scroll-linked interaction…
by thelobbi · MIT
The skill
Expert knowledge for scroll-triggered animations, parallax effects, and scroll-linked interactions that create immersive storytelling experiences.
Scroll Animations Skill
Expert knowledge for scroll-triggered animations, parallax effects, and scroll-linked interactions that create immersive storytelling experiences.
When to Use
Activate this skill when:
- User wants animations triggered by scrolling
- Building parallax scrolling effects
- Creating scroll-linked progress indicators
- Implementing reveal-on-scroll patterns
- Building horizontal scroll sections
- Need scroll-based storytelling
File Patterns
**/*.tsxwith scroll-related hooks**/hooks/useScroll*.ts- Files with
ScrollTriggerimports - Files with
useScroll,useInViewfrom framer-motion - Intersection Observer usage
Scroll Animation Libraries
Framer Motion Scroll
import { motion, useScroll, useTransform, useInView } from 'framer-motion';
GSAP ScrollTrigger
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
Intersection Observer (Native)
const observer = new IntersectionObserver(callback, options);
Framer Motion Patterns
Basic Scroll Progress
import { motion, useScroll, useTransform } from 'framer-motion';
function ScrollProgress() {
const { scrollYProgress } = useScroll();
return (
<motion.div
className="fixed top-0 left-0 h-1 bg-blue-500 origin-left"
style={{ scaleX: scrollYProgress }}
/>
);
}
Parallax Effect
function Parallax() {
const { scrollY } = useScroll();
// Background moves slower than foreground
const backgroundY = useTransform(scrollY, [0, 1000], [0, -200]);
const foregroundY = useTransform(scrollY, [0, 1000], [0, -400]);
return (
<div className="relative h-screen overflow-hidden">
<motion.div
className="absolute inset-0 bg-cover bg-center"
style={{ y: backgroundY, backgroundImage: 'url(bg.jpg)' }}
/>
<motion.div
className="relative z-10"
style={{ y: foregroundY }}
>
<h1>Foreground Content</h1>
</motion.div>
</div>
);
}
Element-Linked Scroll
function ElementParallax() {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start end', 'end start'], // When to start/end tracking
});
const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [0, 1, 0]);
const y = useTransform(scrollYProgress, [0, 1], [100, -100]);
return (
<motion.div
ref={ref}
style={{ opacity, y }}
className="h-screen flex items-center justify-center"
>
Content that fades and moves with scroll
</motion.div>
);
}
useInView for Reveal
import { motion, useInView } from 'framer-motion';
function RevealOnScroll({ children }: { children: React.ReactNode }) {
const ref = useRef(null);
const isInView = useInView(ref, {
once: true, // Only trigger once
margin: '-100px', // Trigger 100px before entering viewport
});
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 50 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, ease: 'easeOut' }}
>
{children}
</motion.div>
);
}
GSAP ScrollTrigger Patterns
Basic Scroll-Triggered Animation
import { useGSAP } from '@gsap/react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
function Component() {
const containerRef = useRef(null);
useGSAP(() => {
gsap.from('.reveal-item', {
y: 100,
opacity: 0,
duration: 1,
stagger: 0.2,
scrollTrigger: {
trigger: '.reveal-container',
start: 'top 80%',
end: 'bottom 20%',
toggleActions: 'play none none reverse',
// markers: true, // Debug
},
});
}, { scope: containerRef });
return (
<div ref={containerRef}>
<div className="reveal-container">
<div className="reveal-item">Item 1</div>
<div className="reveal-item">Item 2</div>
<div className="reveal-item">Item 3</div>
</div>
</div>
);
}
Scrub Animation (Linked to Scroll Position)
useGSAP(() => {
gsap.to('.progress-bar', {
scaleX: 1,
ease: 'none',
scrollTrigger: {
trigger: '.content',
start: 'top top',
end: 'bottom bottom',
scrub: true, // Links animation to scroll position
},
});
});
Pin Section (Sticky Animation)
useGSAP(() => {
const tl = gsap.timeline({
scrollTrigger: {
trigger: '.pin-section',
start: 'top top',
end: '+=300%', // 3x viewport height of scrolling
pin: true,
scrub: 1,
},
});
tl.to('.step-1', { opacity: 0 })
.to('.step-2', { opacity: 1 })
.to('.step-2', { opacity: 0 })
.to('.step-3', { opacity: 1 });
});
Horizontal Scroll Section
useGSAP(() => {
const panels = gsap.utils.toArray('.panel');
gsap.to(panels, {
xPercent: -100 * (panels.length - 1),
ease: 'none',
scrollTrigger: {
trigger: '.horizontal-container',
pin: true,
scrub: 1,
snap: 1 / (panels.length - 1),
end: () => '+=' + document.querySelector('.horizontal-container')!.offsetWidth,
},
});
});
Intersection Observer (Native)
Custom useInView Hook
import { useState, useEffect, useRef, RefObject } from 'react';
interface InViewOptions {
threshold?: number | number[];
rootMargin?: string;
triggerOnce?: boolean;
}
export function useInView<T extends HTMLElement>(
options: InViewOptions = {}
): [RefObject<T>, boolean] {
const { threshold = 0, rootMargin = '0px', triggerOnce = false } = options;
const ref = useRef<T>(null);
const [inView, setInView] = useState(false);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setInView(true);
if (triggerOnce) {
observer.unobserve(element);
}
} else if (!triggerOnce) {
setInView(false);
}
},
{ threshold, rootMargin }
);
observer.observe(element);
return () => observer.disconnect();
}, [threshold, rootMargin, triggerOnce]);
return [ref, inView];
}
// Usage
function Component() {
const [ref, inView] = useInView<HTMLDivElement>({ triggerOnce: true });
return (
<div
ref={ref}
className={`transition-all duration-500 ${
inView ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'
}`}
>
Content reveals when scrolled into view
</div>
);
}
Advanced Patterns
Scroll-Linked Text Reveal
function TextReveal() {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start end', 'end start'],
});
const words = 'This text reveals word by word'.split(' ');
return (
<p ref={ref} className="text-4xl font-bold">
{words.map((word, i) => {
const start = i / words.length;
const end = start + 1 / words.length;
const opacity = useTransform(
scrollYProgress,
[start, end],
[0.2, 1]
);
return (
<motion.span
key={i}
style={{ opacity }}
className="inline-block mr-2"
>
{word}
</motion.span>
);
})}
</p>
);
}
Scroll-Based 3D Card
function Scroll3DCard() {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start end', 'center center'],
});
const rotateX = useTransform(scrollYProgress, [0, 1], [45, 0]);
const scale = useTransform(scrollYProgress, [0, 1], [0.8, 1]);
const opacity = useTransform(scrollYProgress, [0, 0.5], [0, 1]);
return (
<motion.div
ref={ref}
style={{
rotateX,
scale,
opacity,
transformPerspective: 1000,
}}
className="bg-white rounded-xl shadow-xl p-8"
>
Card content
</motion.div>
);
}
Progress-Based Section Navigator
function SectionNavigator() {
const sections = ['intro', 'features', 'pricing', 'contact'];
const { scrollYProgress } = useScroll();
const activeIndex = useTransform(
scrollYProgress,
sections.map((_, i) => i / sections.length),
sections.map((_, i) => i)
);
return (
<nav className="fixed right-4 top-1/2 -translate-y-1/2">
{sections.map((section, i) => (
<motion.button
key={section}
className="block w-3 h-3 rounded-full my-2"
style={{
backgroundColor: useTransform(
activeIndex,
[i - 0.5, i, i + 0.5],
['#ccc', '#3b82f6', '#ccc']
),
}}
onClick={() => {
document.getElementById(section)?.scrollIntoView({ behavior: 'smooth' });
}}
/>
))}
</nav>
);
}
Examples
<example> Context: User wants elements to fade in as they scroll user: "Animate these cards to fade in when scrolling down" assistant: Using Framer Motion's useInView for reveal:
import { motion, useInView } from 'framer-motion';
function FadeInCard({ children, delay = 0 }) {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: '-50px' });
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay, ease: 'easeOut' }}
>
{children}
</motion.div>
);
}
// Usage
<div className="grid grid-cols-3 gap-4">
{cards.map((card, i) => (
<FadeInCard key={card.id} delay={i * 0.1}>
<Card>{card.content}</Card>
</FadeInCard>
))}
</div>
</example>
<example> Context: User needs a parallax hero section user: "Create a parallax scrolling effect for the hero" assistant: Using scroll-linked transforms for parallax:
function ParallaxHero() {
const { scrollY } = useScroll();
const backgroundY = useTransform(scrollY, [0, 500], [0, 150]);
const textY = useTransform(scrollY, [0, 500], [0, -100]);
const opacity = useTransform(scrollY, [0, 300], [1, 0]);
return (
<div className="relative h-screen overflow-hidden">
<motion.img
src="/hero-bg.jpg"
className="absolute inset-0 w-full h-full object-cover"
style={{ y: backgroundY }}
/>
<motion.div
className="relative z-10 flex items-center justify-center h-full"
style={{ y: textY, opacity }}
>
<h1 className="text-6xl font-bold text-white">Welcome</h1>
</motion.div>
</div>
);
}
</example>
Related Skills
- framer-motion - Core animation library
- gsap - Advanced scroll triggers
- transition-engineer - Page transitions
Author
Created by Brookside BI as part of React Animation Studio
Use this skill
Connect your agent to the Skill Store MCP server and ask for it by ref: thelobbi/scroll-animations-85875434486.
Details
- Publisher
- thelobbi
- Category
- Software Development › Frontend & UI
- License
- MIT
- Updated
- 2026-10-08
References
More from thelobbi
- Agentic Design Patterns Catalog of 21 agentic AI design patterns (prompt chaining, routing, parallelization, reflection, too
- Atlassianapi Atlassian API integration for Jira and Confluence automation.
- Authentication Authentication and authorization including JWT, OAuth2, OIDC, sessions, RBAC, and security analysis.
- Aws AWS cloud services including EC2, EKS, S3, Lambda, RDS, and IAM.