GuideJune 2026 · 15 min read · Cornerstone

The complete guide to scroll animations in 2025.

Every scroll animation pattern — fade reveal, parallax, sticky pin, section snap, text split, video scrub, horizontal sections, and CSS variable binding — explained with copy-paste code. All examples use svg-scroll-draw, a single ~9 KB library that covers all of them.

Table of contents

  1. 01 — Fade reveal (the universal pattern)
  2. 02 — Parallax
  3. 03 — Animate any CSS property on scroll
  4. 04 — Sticky / pin sections
  5. 05 — Section snapping
  6. 06 — Text split + reveal
  7. 07 — Animated counters
  8. 08 — Video scrubbing
  9. 09 — Horizontal scroll sections
  10. 10 — CSS variable binding
  11. 11 — Scroll callbacks (onEnter / onLeave)
  12. 12 — Native CSS scroll-driven animations

01

Fade reveal — the universal pattern

The most common scroll animation: elements fade up into view as the user scrolls past them. Every major website uses this. AOS and ScrollReveal.js popularised it. svg-scroll-draw does it in one call with scrollReveal.

app.js
import { scrollReveal } from 'svg-scroll-draw/reveal';

// Default: fade up (opacity 0→1, translateY 32→0)
scrollReveal('.card');

// Custom from state
scrollReveal('.feature', {
  from:    { opacity: 0, y: 40, scale: 0.96 },
  stagger: 0.1,
  easing:  'ease-out',
  once:    true,
});

// Named presets: fadeUp|fadeDown|fadeLeft|fadeRight|scale|flip|flipX
scrollReveal('.badge', { preset: 'scale' });

02

Parallax

Move elements at a different rate than scroll. Background images, floating badges, decorative shapes. Negative speed = opposite direction.

app.js
import { scrollParallax } from 'svg-scroll-draw';

scrollParallax('#hero-bg',       { speed: 0.4 });   // slower than scroll
scrollParallax('#floating-badge',{ speed: -0.2 });  // opposite direction
scrollParallax('#side-element',  { speed: 0.3, axis: 'x' }); // horizontal

03

Animate any CSS property on scroll

Not just opacity and transform — any CSS property can be driven by scroll position. Colors, backgrounds, borders, clip-paths, filter, font-size, anything.

app.js
import { scrollAnimate } from 'svg-scroll-draw';

// Color transition (section background shifts as you scroll through)
scrollAnimate('#section', {
  props: {
    backgroundColor: ['#ffffff', '#0d0d0d'],
    color:           ['#000000', '#ffffff'],
  },
});

// Border radius morph
scrollAnimate('#card', {
  props: { borderRadius: ['0px', '24px'] },
});

// Blur reveal
scrollAnimate('#image', {
  props: { filter: ['blur(20px)', 'blur(0px)'], opacity: [0.3, 1] },
  easing: 'ease-out',
  once:   true,
});

04

Sticky / pin sections

The Apple product page pattern. An element stays fixed while content scrolls past it. Use scrollPin— it wraps the target in a spacer so the page layout doesn't jump.

app.js
import { scrollPin } from 'svg-scroll-draw/pin';

// Pin product image while feature text scrolls
scrollPin('#product-image', {
  top:         80,                     // 80px from viewport top (below nav)
  pinDistance: window.innerHeight * 3, // stay pinned for 3 viewport heights
  onEnter:     () => image.classList.add('active'),
  onLeave:     () => image.classList.remove('active'),
  onEnterBack: () => image.classList.add('active'),
  onLeaveBack: () => image.classList.remove('active'),
});

05

Section snapping

Snap the viewport to the nearest section when the user stops scrolling. Custom easing, configurable threshold, programmatic control.

app.js
import { scrollSnap } from 'svg-scroll-draw/snap';

const snap = scrollSnap('.section', {
  duration:  600,
  easing:    'ease-in-out',
  threshold: 0.3,            // snap if scrolled 30%+ past a section
  onSnap:    (index) => updateNav(index),
});

// Programmatic
snap.snapTo(2);              // smooth scroll to section 2
snap.getCurrentIndex();      // → active section index
snap.destroy();

06

Text split + reveal

Split text into chars, words, or lines and stagger-animate each piece. Free replacement for GSAP SplitText (which requires a paid Club GreenSock subscription).

app.js
import { scrollText } from 'svg-scroll-draw/text';

// Word-by-word headline reveal
scrollText('#headline', {
  split:   'words',
  stagger: 0.07,
  from:    { opacity: 0, y: 32 },
  easing:  'ease-out',
  once:    true,
});

// Typewriter (char by char)
scrollText('#subtitle', {
  split:   'chars',
  stagger: 0.015,
  from:    { opacity: 0 },
  easing:  'linear',
  once:    true,
});

07

Animated counters

Numbers that count up as they scroll into view. Stats sections, pricing, social proof.

app.js
import { scrollCounter } from 'svg-scroll-draw';

scrollCounter('#users',      { to: 50000, format: n => Math.round(n).toLocaleString() + '+' });
scrollCounter('#revenue',    { to: 1250000, format: n => '$' + Math.round(n).toLocaleString() });
scrollCounter('#nps',        { to: 94.7, decimals: 1, format: n => n.toFixed(1) + '%' });
scrollCounter('#bundle',     { to: 9, format: n => '~' + Math.round(n) + ' KB' });

08

Video scrubbing

Tie <video>.currentTime to scroll position. The Apple / Stripe product video pattern — free, no GSAP needed.

app.js
import { scrollVideo } from 'svg-scroll-draw/video';

scrollVideo('#hero-video', {
  trigger: { start: 'top top', end: 'bottom top' },
});

09

Horizontal scroll sections

Vertical scroll drives horizontal movement. Set up sticky CSS, one call drives the transform.

app.js
import { scrollHorizontal } from 'svg-scroll-draw/horizontal';

// CSS: .outer{height:400vh} .sticky{position:sticky;top:0;height:100vh;overflow:hidden}
// .track{display:flex;width:max-content}

scrollHorizontal('.track', {
  distance: document.querySelector('.track').scrollWidth - window.innerWidth,
  easing:   'linear',
});

10

CSS variable binding

Expose scroll progress as a CSS custom property. Then CSS calc() drives everything — color, size, position, filter — with zero extra JS.

app.js
import { scrollProgress } from 'svg-scroll-draw/progress';

scrollProgress('#section', { easing: 'ease-in-out' });
styles.css
#section {
  /* Drive any CSS property from the variable */
  opacity: calc(var(--scroll-progress-eased));
  transform: translateY(calc((1 - var(--scroll-progress-eased)) * 40px));
  background: hsl(
    calc(240 + var(--scroll-progress) * 120),
    60%, 10%
  );
}

11

Scroll callbacks (onEnter / onLeave)

Fire code when scroll crosses the trigger zone — in either direction. Nav highlighting, lazy loading, analytics events, state updates.

app.js
import { scrollAnimate } from 'svg-scroll-draw';

scrollAnimate('#section', {
  props: { opacity: [0.4, 1] },
  trigger: { start: 'top center', end: 'bottom center' },
  onEnter:     () => nav.setActive('section'),
  onLeave:     () => nav.clearActive('section'),
  onEnterBack: () => nav.setActive('section'),
  onLeaveBack: () => nav.clearActive('section'),
});

12

Native CSS scroll-driven animations

When animation-timeline: view() is supported (Chrome 115+, Edge 115+), svg-scroll-draw automatically uses it — zero JS scroll listeners, zero rAF, pure compositor animation. Falls back silently in older browsers.

app.js
import { scrollAnimate } from 'svg-scroll-draw';

// native: true is the default — uses CSS fast path when eligible
scrollAnimate('#hero', {
  props: { opacity: [0, 1], transform: ['translateY(32px)', 'translateY(0)'] },
  // native: true (default) — runs on compositor when supported
});

// Force JS engine (needed for callbacks, velocity, custom easing fns)
scrollAnimate('#section', {
  props: { opacity: [0, 1] },
  native: false,
  onEnter: () => activate(),
});

One library, all patterns.

scrollRevealscrollAnimatescrollParallaxscrollPinscrollSnapscrollTextscrollCounterscrollVideoscrollHorizontalscrollProgressscrollDrawdevtools