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.
Every snippet in this guide runs in the browser. In the Next.js App Router, put it in a client component and keep the page on the server; the Next.js scroll animation guide has a complete App Router project to copy.
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' });Parallax
Move elements at a different rate than scroll. Background images, floating badges, decorative shapes. A positive speed moves the element up as you scroll down, ahead of the page; a negative one moves it down, so it lags behind.
import { scrollParallax } from 'svg-scroll-draw';
scrollParallax('#hero-bg', { speed: 0.4 }); // ahead of the page: up 40% of its height
scrollParallax('#floating-badge',{ speed: -0.2 }); // lags behind the page
scrollParallax('#side-element', { speed: 0.3, axis: 'x' }); // horizontalAnimate 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.
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,
});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.
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: '300vh', // 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'),
});Section snapping
Snap the viewport to the nearest section when the user stops scrolling. Custom easing, configurable threshold, programmatic control.
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();Text split + reveal
Split text into chars, words, or lines and stagger-animate each piece. A 3.4 KB alternative to GSAP SplitText. SplitText is free and 3.6 KB, but it only splits — animating the pieces needs GSAP core (27.7 KB) too.
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,
});Animated counters
Numbers that count up as they scroll into view. Stats sections, pricing, social proof.
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, format: n => n.toFixed(1) + '%' }); // decimals would replace format
scrollCounter('#bundle', { to: 12, format: n => '~' + Math.round(n) + ' KB' });Video scrubbing
Tie <video>.currentTime to scroll position. The Apple / Stripe product video pattern — free, no GSAP needed.
import { scrollVideo } from 'svg-scroll-draw/video';
scrollVideo('#hero-video', {
trigger: { start: 'top top', end: 'bottom top' },
});Horizontal scroll sections
Vertical scroll drives horizontal movement. Set up sticky CSS, one call drives the transform.
import { scrollHorizontal } from 'svg-scroll-draw/horizontal';
// CSS: .outer{height:400vh} .sticky{position:sticky;top:0;height:100vh;overflow:clip}
// .track{display:flex;width:max-content}
// The travel is measured from the track and its sticky stage, and again
// whenever the layout changes.
scrollHorizontal('.track');The whole thing as a recipe, a case-study strip in five steps with its React and HTML files and where it goes wrong, is in Horizontal scroll sections without GSAP.
CSS variable binding
Expose scroll progress as a CSS custom property. Then CSS calc() drives everything — color, size, position, filter — with zero extra JS.
import { scrollProgress } from 'svg-scroll-draw/progress';
scrollProgress('#section', { easing: 'ease-in-out' });#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%
);
}Scroll callbacks (onEnter / onLeave)
Fire code when scroll crosses the trigger zone — in either direction. Nav highlighting, lazy loading, analytics events, state updates.
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'),
});Native CSS scroll-driven animations
When animation-timeline: view() is supported (Chrome and Edge 115+, Safari 26+, Firefox 159+), svg-scroll-draw automatically uses it — zero JS scroll listeners, zero rAF; for opacity and transform the browser can run it on the compositor. Falls back silently in older browsers.
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) — CSS scroll timeline when supported; opacity and transform can run on the compositor
});
// Force JS engine (needed for callbacks, velocity, custom easing fns)
scrollAnimate('#section', {
props: { opacity: [0, 1] },
native: false,
onEnter: () => activate(),
});What makes a config eligible, and what the native path saves, is in Zero-JS SVG scroll animations with native CSS.
One library, all patterns
| API | Import from | Pattern |
|---|---|---|
| scrollReveal | svg-scroll-draw/reveal | 01 · Fade reveal |
| scrollParallax | svg-scroll-draw | 02 · Parallax |
| scrollAnimate | svg-scroll-draw | 03 · Any CSS property |
| scrollPin | svg-scroll-draw/pin | 04 · Sticky / pin |
| scrollSnap | svg-scroll-draw/snap | 05 · Section snapping |
| scrollText | svg-scroll-draw/text | 06 · Text split + reveal |
| scrollCounter | svg-scroll-draw | 07 · Animated counters |
| scrollVideo | svg-scroll-draw/video | 08 · Video scrubbing |
| scrollHorizontal | svg-scroll-draw/horizontal | 09 · Horizontal sections |
| scrollProgress | svg-scroll-draw/progress | 10 · CSS variable binding |
| onEnter / onLeave | svg-scroll-draw | 11 · Scroll callbacks |
| scrollDraw · scrollAnimate | svg-scroll-draw | 12 · Native CSS timelines |
| scrollDraw | svg-scroll-draw | SVG path drawing — the library’s namesake |
| devtools | svg-scroll-draw/devtools | A development overlay of each instance’s trigger window |
— / next
One library, all patterns
Each pattern is its own entry point, so you ship only what you import — scrollPin alone is 1.6 KB, scrollReveal 5.8 KB, the main entry 11.6 KB. MIT, zero dependencies.