svg-scroll-draw
| Install | |
|---|---|
npm install svg-scroll-draw |
|
| Latest Version: | 2.10.0 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Sep 11, 2026 |
| Links: | GitHub · npm |
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-draw
scrollAnimate — 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' }); // horizontal
speed 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 up
Easing
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 seconds
Sub-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 init
Scaffolds 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
Related Packages
Accessible (WCAG 2.2 AA / ATAG 2.0), RTL-first, CSP-safe rich text editor for an...
Framework-agnostic animated handwriting renderer with React, Svelte, Vue, Nuxt,...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 2.10.0 | >=3 | MIT | |
| 2.9.0 | >=3 | MIT | |
| 2.7.0 | >=3 | MIT | |
| 2.6.0 | >=3 | MIT | |
| 2.2.0 | >=3 | MIT | |
| 1.7.0 | >=3 | MIT | |
| 1.6.0 | >=3 | MIT | |
| 1.5.0 | >=3 | MIT | |
| 1.4.0 | >=3 | MIT | |
| 1.3.0 | >=3 | MIT | |
| 1.2.0 | >=3 | MIT | |
| 1.1.0 | >=3 | MIT | |
| 1.0.0 | >=3 | MIT | |
| 0.7.0 | >=3 | MIT | |
| 0.6.2 | >=3 | MIT | |
| 0.6.0 | >=3 | MIT | |
| 0.5.0 | >=3 | MIT | |
| 0.4.0 | >=3 | MIT | |
| 0.3.0 | >=3 | MIT | |
| 0.2.1 | >=3 | MIT | |
| 0.2.0 | >=3 | ||
| 0.1.0 |