# Scroll Animations Reference ## Intersection Observer Hook ```tsx import { useEffect, useRef, useState, type RefObject } from 'react'; interface UseInViewOptions { threshold?: number | number[]; rootMargin?: string; triggerOnce?: boolean; } function useInView({ threshold = 0, rootMargin = '0px', triggerOnce = false, }: UseInViewOptions = {}): [RefObject, boolean] { const ref = useRef(null); const [isInView, setIsInView] = useState(false); useEffect(() => { const element = ref.current; if (!element) return; const observer = new IntersectionObserver( ([entry]) => { const inView = entry.isIntersecting; setIsInView(inView); if (inView && triggerOnce) { observer.unobserve(element); } }, { threshold, rootMargin } ); observer.observe(element); return () => observer.disconnect(); }, [threshold, rootMargin, triggerOnce]); return [ref, isInView]; } // Usage function FadeInSection({ children }) { const [ref, isInView] = useInView({ threshold: 0.2, triggerOnce: true }); return (
{children}
); } ``` ## Scroll Progress Indicator ```tsx import { motion, useScroll, useSpring } from 'framer-motion'; function ScrollProgress() { const { scrollYProgress } = useScroll(); const scaleX = useSpring(scrollYProgress, { stiffness: 100, damping: 30, restDelta: 0.001, }); return ( ); } ``` ## Parallax Scrolling ### Simple CSS Parallax ```css .parallax-container { height: 100vh; overflow-x: hidden; overflow-y: auto; perspective: 10px; } .parallax-layer-back { transform: translateZ(-10px) scale(2); } .parallax-layer-base { transform: translateZ(0); } ``` ### Framer Motion Parallax ```tsx import { motion, useScroll, useTransform } from 'framer-motion'; function ParallaxHero() { const ref = useRef(null); const { scrollYProgress } = useScroll({ target: ref, offset: ['start start', 'end start'], }); const y = useTransform(scrollYProgress, [0, 1], ['0%', '50%']); const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0]); const scale = useTransform(scrollYProgress, [0, 1], [1, 1.2]); return (
{/* Background image with parallax */} {/* Content fades out on scroll */}

Welcome

); } ``` ## Scroll-Linked Animations ### Progress-Based Animation ```tsx function ScrollAnimation() { const containerRef = useRef(null); const { scrollYProgress } = useScroll({ target: containerRef, offset: ['start end', 'end start'], }); // Different transformations based on scroll progress const x = useTransform(scrollYProgress, [0, 1], [-200, 200]); const rotate = useTransform(scrollYProgress, [0, 1], [0, 360]); const backgroundColor = useTransform( scrollYProgress, [0, 0.5, 1], ['#3b82f6', '#8b5cf6', '#ec4899'] ); return (
); } ``` ### Horizontal Scroll Section ```tsx function HorizontalScroll({ items }) { const containerRef = useRef(null); const { scrollYProgress } = useScroll({ target: containerRef, offset: ['start start', 'end end'], }); const x = useTransform( scrollYProgress, [0, 1], ['0%', `-${(items.length - 1) * 100}%`] ); return (
{items.map((item, i) => (
{item}
))}
); } ``` ## Reveal Animations ### Staggered List Reveal ```tsx function StaggeredList({ items }) { const [ref, isInView] = useInView({ threshold: 0.1, triggerOnce: true }); return (
    {items.map((item, i) => ( {item.content} ))}
); } ``` ### Text Reveal ```tsx function TextReveal({ text }) { const [ref, isInView] = useInView({ threshold: 0.5, triggerOnce: true }); const words = text.split(' '); return (

{words.map((word, i) => ( {word} ))}

); } ``` ### Clip Path Reveal ```tsx function ClipReveal({ children }) { const [ref, isInView] = useInView({ threshold: 0.3, triggerOnce: true }); return ( {children} ); } ``` ## Sticky Scroll Sections ```tsx function StickySection({ title, content, image }) { const ref = useRef(null); const { scrollYProgress } = useScroll({ target: ref, offset: ['start start', 'end start'], }); const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [1, 1, 0]); const scale = useTransform(scrollYProgress, [0, 0.5, 1], [1, 1, 0.8]); return (

{title}

{content}

); } ``` ## Smooth Scroll ```tsx // Using CSS html { scroll-behavior: smooth; } // Using Lenis for butter-smooth scrolling import Lenis from '@studio-freight/lenis'; function SmoothScrollProvider({ children }) { useEffect(() => { const lenis = new Lenis({ duration: 1.2, easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), direction: 'vertical', smooth: true, }); function raf(time) { lenis.raf(time); requestAnimationFrame(raf); } requestAnimationFrame(raf); return () => lenis.destroy(); }, []); return children; } ``` ## Scroll Snap ```css /* Scroll snap container */ .snap-container { scroll-snap-type: y mandatory; overflow-y: scroll; height: 100vh; } .snap-section { scroll-snap-align: start; height: 100vh; } /* Smooth scrolling with snap */ @supports (scroll-snap-type: y mandatory) { .snap-container { scroll-behavior: smooth; } } ``` ```tsx function FullPageScroll({ sections }) { return (
{sections.map((section, i) => (
{section}
))}
); } ``` ## Performance Optimization ```tsx // Use will-change sparingly const AnimatedElement = styled(motion.div)` will-change: transform; `; // Debounce scroll handlers function useThrottledScroll(callback, delay = 16) { const lastRun = useRef(0); useEffect(() => { const handler = () => { const now = Date.now(); if (now - lastRun.current >= delay) { lastRun.current = now; callback(); } }; window.addEventListener('scroll', handler, { passive: true }); return () => window.removeEventListener('scroll', handler); }, [callback, delay]); } // Use transform instead of top/left // Good const goodAnimation = { transform: 'translateY(100px)' }; // Bad (causes reflow) const badAnimation = { top: '100px' }; ```