svg-scroll-draw
v2.10.0
Published
Scroll-driven animation for the web — draw SVG paths, reveal elements, pin sections, scrub video. Zero dependencies, native CSS fast path, works with React, Vue, Svelte, Solid, Angular, Astro and vanilla JS
Downloads
180
Maintainers
Keywords
Readme
svg-scroll-draw
The scroll animation platform. Animate SVG paths, CSS properties, counters, video, and text — all scroll-driven, all MIT, all under 10 KB. GSAP is overkill.
Live Demo · npm · Docs · Examples · Report a bug
What's in v2
| API | Import | What it does |
|---|---|---|
| scrollAnimate | svg-scroll-draw | Animate any CSS property on any element driven by scroll. Replaces gsap.to + ScrollTrigger. |
| scrollCounter | svg-scroll-draw | Animate a number from from to to as it scrolls into view. Custom format, decimals, easing. |
| scrollParallax | svg-scroll-draw | Move any element at a different rate than scroll. Speed multiplier, reverse direction. |
| scrollDraw | svg-scroll-draw | The original — scroll-driven SVG path drawing via stroke-dashoffset. |
| scrollVideo | svg-scroll-draw/video | Tie <video>.currentTime to scroll. The Apple/Stripe product-page pattern. |
| scrollText | svg-scroll-draw/text | Split text into chars/words/lines and stagger-animate each on scroll. Free GSAP SplitText. |
| devtools | svg-scroll-draw/devtools | Visual overlay showing every active animation's triggers and progress. Dev-only. |
All APIs return the same instance object: { destroy, replay, pause, resume, seek, getProgress }.
Install
npm i svg-scroll-draw
# pnpm add svg-scroll-draw
# yarn add svg-scroll-draw
# bun add svg-scroll-drawscrollAnimate — any CSS property on scroll
import { scrollAnimate } from 'svg-scroll-draw';
// Fade + slide in
scrollAnimate('#hero-text', {
props: {
opacity: [0, 1],
transform: ['translateY(40px)', 'translateY(0px)'],
},
easing: 'ease-out',
once: true,
});
// Color transition
scrollAnimate('#section', {
props: {
backgroundColor: ['#ffffff', '#0d0d0d'],
color: ['#000000', '#ffffff'],
},
trigger: { start: 'top 60%', end: 'top 20%' },
});
// Multiple elements with stagger
document.querySelectorAll('.card').forEach((el, i) => {
scrollAnimate(el, {
props: { opacity: [0, 1], transform: ['translateY(32px)', 'translateY(0)'] },
trigger: { start: `top ${85 - i * 5}%`, end: `top ${50 - i * 5}%` },
easing: 'ease-out',
once: true,
});
});React
import { ScrollAnimate } from 'svg-scroll-draw/react';
<ScrollAnimate
props={{ opacity: [0, 1], transform: ['translateY(24px)', 'translateY(0)'] }}
easing="ease-out"
once
>
<div>Any content — HTML or SVG</div>
</ScrollAnimate>scrollAnimate options
| Option | Type | Default | Description |
|---|---|---|---|
| props | Record<string, [from, to]> | — | CSS properties to animate. Supports numbers, hex/rgb colors, transform functions, and unit values like 40px. |
| trigger.start | string | "top bottom" | When animation begins |
| trigger.end | string | "bottom top" | When animation ends |
| easing | string \| fn | "ease-out" | Same easing system as scrollDraw |
| speed | number | 1 | Animation scale factor |
| once | boolean | false | Freeze at max progress — doesn't reverse on scroll back |
| axis | "x" \| "y" | "y" | Scroll axis |
| native | boolean | true | Use CSS animation-timeline: view() fast path when eligible |
| onProgress | (n: number) => void | — | Called every frame with alpha 0–1 |
| onComplete | () => void | — | Fires when alpha reaches 1 |
scrollCounter — animated number on scroll
import { scrollCounter } from 'svg-scroll-draw';
// Simple count-up
scrollCounter('#users', { to: 50_000, once: true });
// Formatted currency
scrollCounter('#revenue', {
to: 1_250_000,
format: n => '$' + Math.round(n).toLocaleString(),
easing: 'ease-out',
once: true,
});
// Percentage with decimal
scrollCounter('#rate', {
from: 0,
to: 94.7,
decimals: 1,
format: n => n.toFixed(1) + '%',
});React
import { ScrollCounter } from 'svg-scroll-draw/react';
<ScrollCounter to={50000} format={n => n.toLocaleString()} once />scrollCounter options
| Option | Type | Default | Description |
|---|---|---|---|
| to | number | — | Target value (required) |
| from | number | 0 | Starting value. Set higher than to to count down. |
| format | (n: number) => string | String(Math.round(n)) | Custom formatter |
| decimals | number | — | Shorthand for format: n => n.toFixed(decimals) |
| easing | string \| fn | "ease-out" | — |
| trigger | TriggerConfig | { start: 'top 80%', end: 'top 20%' } | — |
| once | boolean | true | — |
| onComplete | () => void | — | — |
scrollParallax — parallax in one line
import { scrollParallax } from 'svg-scroll-draw';
scrollParallax('#hero-bg', { speed: 0.4 }); // 40% of scroll rate
scrollParallax('#floating-badge', { speed: -0.3 }); // opposite direction
scrollParallax('#sidebar', { speed: 0.3, axis: 'x' }); // horizontalspeed is a multiplier relative to element height. 0.5 = half scroll speed, -0.2 = opposite direction.
scrollVideo — video scrubbing on scroll
import { scrollVideo } from 'svg-scroll-draw/video';
// Scrub the full video as user scrolls through the section
scrollVideo('#hero-video', {
trigger: { start: 'top top', end: 'bottom top' },
});
// Scrub only the first 3 seconds
scrollVideo('#product-reveal', {
from: 0,
to: 3,
trigger: { start: 'top 80%', end: 'top 20%' },
easing: 'ease-in-out',
});<!-- Recommended attributes for smooth scrubbing -->
<video id="hero-video" src="hero.mp4" muted playsinline preload="auto"></video>React
import { ScrollVideo } from 'svg-scroll-draw/react';
<ScrollVideo src="/hero.mp4" trigger={{ start: 'top top', end: 'bottom top' }} />scrollVideo options
| Option | Type | Default | Description |
|---|---|---|---|
| from | number | 0 | Start time in seconds |
| to | number | video.duration | End time in seconds |
| trigger | TriggerConfig | { start: 'top top', end: 'bottom top' } | — |
| easing | string \| fn | "linear" | — |
| once | boolean | false | — |
| preload | "auto" \| "metadata" | "auto" | Sets preload attribute if not already present |
| onReady | () => void | — | Fires when video metadata is loaded |
| onProgress | (n: number) => void | — | — |
| onComplete | () => void | — | — |
scrollText — text reveal on scroll
Free replacement for GSAP SplitText (which requires a $150+/yr Club GreenSock subscription).
import { scrollText } from 'svg-scroll-draw/text';
// Words fade up one by one
scrollText('#headline', {
split: 'words',
stagger: 0.05,
from: { opacity: 0, y: 24 },
easing: 'ease-out',
once: true,
});
// Characters with rotation
scrollText('#tagline', {
split: 'chars',
stagger: 0.025,
from: { opacity: 0, y: 32, rotate: 8 },
once: true,
});React
import { ScrollText } from 'svg-scroll-draw/react';
<ScrollText split="words" stagger={0.05} from={{ opacity: 0, y: 24 }} once>
Animate this headline word by word.
</ScrollText>scrollText options
| Option | Type | Default | Description |
|---|---|---|---|
| split | "chars" \| "words" \| "lines" | "words" | How to split the text |
| stagger | number | 0.04 | Delay between each unit (0–1 fraction of the animation range) |
| from | { opacity?, y?, x?, rotate?, scale? } | { opacity: 0, y: 24 } | Starting state of each unit |
| easing | string \| fn | "ease-out" | — |
| trigger | TriggerConfig | { start: 'top 85%', end: 'top 40%' } | — |
| once | boolean | true | — |
| onComplete | () => void | — | — |
Accessibility:
scrollTextpreserves the original text inaria-labelon the container and addsaria-hidden="true"to all split spans.destroy()fully restores the original HTML.
scrollDraw — SVG path drawing (v1 API, unchanged)
import { scrollDraw } from 'svg-scroll-draw';
scrollDraw('#my-svg', {
easing: 'ease-out',
speed: 1.2,
fade: true,
once: true,
stagger: 0.15,
trigger: { start: 'top 80%', end: 'top 20%' },
});React / Next.js
import { ScrollDraw } from 'svg-scroll-draw/react';
export default function Hero() {
return (
<ScrollDraw speed={1.2} fade easing="ease-out" once>
<svg width="500" height="500" viewBox="0 0 500 500">
<path d="M10 80 C 40 10, 60 10, 95 80" stroke="black" fill="none" />
</svg>
</ScrollDraw>
);
}Next.js App Router: add
'use client'to any component that uses these wrappers.
Vue 3
<script setup>
import { ScrollDraw } from 'svg-scroll-draw/vue';
</script>
<template>
<ScrollDraw easing="ease-out" :speed="1.2">
<svg>...</svg>
</ScrollDraw>
</template>scrollDraw options
| Option | Type | Default | Description |
|---|---|---|---|
| selector | string | "path, polyline…" | CSS selector for child elements to animate |
| speed | number | 1 | Scale factor relative to scroll distance |
| fade | boolean | false | Animate opacity 0 → 1 simultaneously |
| easing | string \| fn | "linear" | linear, ease-in, ease-out, ease-in-out, spring, bounce, elastic, or (t) => t |
| stagger | number | 0 | Offset between each path starting (fraction of scroll range) |
| direction | "forward" \| "reverse" | "forward" | reverse erases the path as you scroll |
| trigger.start | string | "top bottom" | When animation begins |
| trigger.end | string | "bottom top" | When animation ends |
| once | boolean | false | Lock at max progress |
| debug | boolean | false | Show trigger zone overlay (dev only) |
| axis | "x" \| "y" | "y" | Scroll axis |
| scrollContainer | string \| Element | window | Custom scroll container |
| autoReverse | boolean | false | Reverse when scrolling back up |
| delay | number | 0 | ms before the engine starts observing |
| strokeColor | string \| [string, string] | — | Static color or [from, to] animation |
| strokeWidth | number \| [number, number] | — | Static width or [from, to] animation |
| fillOpacity | number \| [number, number] | — | Animate fill opacity in sync with the draw |
| clip | boolean \| "left" \| "right" \| "top" \| "bottom" \| "center" | false | Reveal via CSS clip-path instead of stroke-dashoffset |
| morphTo | string | — | SVG path d value to morph toward |
| waypoints | Record<number, () => void> | — | Callbacks at specific progress thresholds (0–1) |
| velocityScale | boolean \| number | false | Scale draw speed by scroll velocity |
| repeat | number \| "infinite" | 0 | Replay N times after completion |
| repeatDelay | number | 0 | ms between repeats |
| autoplay | boolean | false | Trigger on viewport entry instead of scroll |
| duration | number | 1000 | Duration in ms for autoplay mode |
| native | boolean | true | Use CSS animation-timeline: view() when eligible |
| preset | "sketch" \| "reveal" \| "typewriter" \| "cinematic" \| "spring" | — | Apply a named option preset as the base config |
| onProgress | (alpha: number) => void | — | Called every frame with draw progress (0–1) |
| onStart | () => void | — | Fires on the first frame |
| onComplete | () => void | — | Fires when all paths reach 100% |
Instance API
Every function returns the same instance object:
const inst = scrollAnimate('#el', { props: { opacity: [0, 1] } });
// Same for scrollDraw, scrollCounter, scrollVideo, scrollText, scrollParallax
inst.pause(); // pause at current frame
inst.resume(); // resume from paused state
inst.seek(0.5); // jump to 50% and pause
inst.replay(); // reset and replay from the beginning
inst.getProgress(); // → number 0–1
inst.destroy(); // remove all listeners and clean upEasing
All APIs share the same easing system:
// Named strings
easing: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'spring' | 'bounce' | 'elastic'
// Custom function
easing: (t: number) => number
// Factory functions for parameterized physics easings
import { createSpring, createBounce, createElastic } from 'svg-scroll-draw';
easing: createSpring({ tension: 3, friction: 1.8 })
easing: createBounce({ bounces: 4, decay: 0.4 })
easing: createElastic({ amplitude: 1.5, period: 0.3 })Presets
Named option bags for scrollDraw:
import { scrollDraw, PRESETS } from 'svg-scroll-draw';
scrollDraw('#logo', { preset: 'sketch' }); // staggered ease-in, pencil feel
scrollDraw('#hero', { preset: 'reveal' }); // fade + ease-out, once
scrollDraw('#type', { preset: 'typewriter' }); // fast linear stagger
scrollDraw('#film', { preset: 'cinematic' }); // slow ease-in-out + fade
scrollDraw('#icon', { preset: 'spring' }); // spring easing
// Inspect or extend
console.log(PRESETS.sketch);DevTools
Visual debug overlay — shows every active animation's trigger window, progress bar, and type. Zero production bytes.
import { devtools } from 'svg-scroll-draw/devtools';
devtools.enable(); // Cmd/Ctrl+Shift+S to toggle
devtools.disable();
devtools.highlight('#my-element'); // outline for 2 secondsSub-path exports
| Import | Contents |
|---|---|
| svg-scroll-draw | scrollDraw, scrollAnimate, scrollCounter, scrollParallax, PRESETS, easing factories |
| svg-scroll-draw/react | ScrollDraw, ScrollAnimate, ScrollCounter, ScrollVideo, ScrollText, useScrollDrawProgress |
| svg-scroll-draw/vue | ScrollDraw, useScrollDraw · v2: ScrollAnimate, ScrollCounter, ScrollVideo, ScrollText, useScrollAnimate, useScrollCounter, useScrollVideo, useScrollText |
| svg-scroll-draw/svelte | scrollDraw action, createScrollDraw · v2: scrollAnimate, scrollCounterAction, scrollVideoAction, scrollTextAction, createScrollAnimate, createScrollCounter, createScrollVideo, createScrollText |
| svg-scroll-draw/solid | useScrollDraw, createScrollDraw · v2: useScrollAnimate, useScrollCounter, useScrollVideo, useScrollText, createScrollAnimate, createScrollCounter, createScrollVideo, createScrollText |
| svg-scroll-draw/angular | ScrollDrawRef · v2: ScrollAnimateRef, ScrollCounterRef, ScrollVideoRef, ScrollTextRef |
| svg-scroll-draw/astro | initScrollDraw() · v2: initScrollAnimate(), initScrollCounter(), initScrollText(), initAll() |
| svg-scroll-draw/nuxt | All Vue v1+v2 composables and components, createScrollDrawPlugin() |
| svg-scroll-draw/video | scrollVideo |
| svg-scroll-draw/text | scrollText |
| svg-scroll-draw/group | scrollDrawGroup, scrollDrawSequence · v2: scrollAnimateGroup, scrollAnimateSequence, scrollParallaxGroup |
| svg-scroll-draw/timeline | scrollDrawTimeline |
| svg-scroll-draw/cinematic | Cinematic |
| svg-scroll-draw/devtools | devtools (dev-only) |
Bundle sizes
| Entry | Gzipped |
|---|---|
| svg-scroll-draw (main) | ~9 KB |
| svg-scroll-draw/react | ~3 KB |
| svg-scroll-draw/video | ~1.5 KB |
| svg-scroll-draw/text | ~2 KB |
| svg-scroll-draw/devtools | ~4 KB (dev only) |
GSAP core + ScrollTrigger = ~40 KB gzipped. svg-scroll-draw v2 covers 95% of those use cases at ~9 KB.
CDN / Web Component
<script src="https://unpkg.com/svg-scroll-draw/dist/cdn/svg-scroll-draw.global.js"></script>
<scroll-draw easing="ease-out" speed="1.2">
<svg>...</svg>
</scroll-draw>
<script>
SvgScrollDraw.scrollDraw('#container', { easing: 'ease-out' });
SvgScrollDraw.scrollAnimate('#hero', {
props: { opacity: [0, 1] }, easing: 'ease-out', once: true,
});
</script>CLI
npx svg-scroll-draw initScaffolds a ready-to-use starter file for React, Vue, Svelte, Solid, or Vanilla JS.
Browser support
Chrome 80+, Safari 14+, Firefox 75+, Edge 80+
Native CSS animation-timeline: view() fast path: Chrome 115+, Safari 18+, Firefox 110+. Automatically falls back to the JS engine on older browsers.
License
MIT
