Scroll animations

Scroll-driven motion with restraint, not parallax for its own sake

Scroll-driven animation ties motion to scroll position instead of a clock. If it doesn't serve orientation, feedback, or continuity, remove it. ## CSS scroll-driven animations ### Scroll progress timeline ```css @keyframes fade-in { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .hero-title { animation: fade-in linear both; animation-timeline: scroll(); animation-range: 0% 30%; } ``` `animation-range: 0% 30%` means the animation completes at 30% scroll depth. ### View progress timeline ```css .card { animation: fade-in linear both; animation-timeline: view(); animation-range: entry 0% entry 100%; } ``` `entry 0%` = first touches the viewport. `entry 100%` = fully visible. ## IntersectionObserver patterns ```typescript const observer = new IntersectionObserver( (entries) => { for (const entry of entries) { if (entry.isIntersecting) { entry.target.classList.add("visible"); observer.unobserve(entry.target); } } }, { threshold: 0.2 } ); document.querySelectorAll("[data-animate]").forEach((el) => { observer.observe(el); }); ``` Paired with CSS: ```css [data-animate] { opacity: 0; transform: translateY(16px); transition: opacity 400ms ease-out, transform 400ms ease-out; } [data-animate].visible { opacity: 1; transform: translateY(0); } ``` ## Parallax ```css .parallax-container { perspective: 1px; overflow-y: auto; height: 100vh; } .parallax-bg { transform: translateZ(-1px) scale(2); } ``` `scale(2)` compensates for size reduction from the Z-axis offset. **Use for:** Hero sections, landing pages. **Skip on:** Content-heavy pages, documentation, dashboards, mobile. ## Performance **Only animate `transform` and `opacity`.** **Batch JavaScript scroll handlers** with `requestAnimationFrame`: ```typescript let ticking = false; window.addEventListener("scroll", () => { if (!ticking) { requestAnimationFrame(() => { updateParallax(); ticking = false; }); ticking = true; } }); ``` ## Accessibility ```css @media (prefers-reduced-motion: reduce) { .parallax-bg { transform: none; } [data-animate] { opacity: 1; transform: none; transition: none; } * { animation-timeline: initial !important; } } ``` } /> ## Common mistakes **Scroll-jacking.** Hijacking native scroll breaks back/forward, bookmarks, find-on-page, and screen readers. **Hiding content behind scroll.** Reveal content already in the DOM with `opacity: 0`, not dynamically injected content. **Momentum scroll on mobile.** iOS fires scroll events at irregular intervals. Keep mobile scroll animations to IntersectionObserver triggers, not continuous scrubbing.