svg-scroll-draw
v2.9.0
Published
Scroll-driven SVG path drawing animation library — zero dependencies, ~4.4 KB gzipped, works with React, Vue, and vanilla JS
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
