ANIMATE SVG
PATHS AS YOU SCROLL.
The scroll animation platform. ~9 KB gzipped. Works in React, Vue 3, Svelte, Solid, and more.
Live on this page
This page
draws itself.
The fountain pen illustration to the right isn't a video or a GIF. It's a live SVG animated by svg-scroll-draw — the exact same package you'd install. Scroll down and watch the ink ribbon draw out in real time.
<ScrollDraw
selector=".ink-ribbon"
easing="ease-out"
speed={1.5}
>
<svg>{/* fountain pen */}</svg>
</ScrollDraw>The problem
Every existing tool
is broken.
Overkill for a single effect — and requires a Club GreenSock subscription for commercial use.
Locks you into one ecosystem and adds 35KB of runtime overhead just to draw a line.
Requires manually targeting individual path IDs. Crashes in Next.js with window is not defined.
Bundle size
~9 KB.
Not 40 KB.
Sizes are minified + gzipped. GSAP DrawSVG requires a paid Club GreenSock license for commercial use. Full GSAP comparison →
v1.1.0
Native CSS, with
a safety net.
On Chrome, Edge, and Firefox the simple draw case runs on the compositor via animation-timeline: view() — zero per-frame JavaScript, no scroll listeners, no rAF loop. When the browser doesn't support it, or the config uses a feature CSS can't express declaratively, the library falls back to the JS engine automatically. You never change your code.
Runs on the compositor when you use: a default or named trigger, a named easing, optional fade, forward or reverse direction.
<ScrollDraw easing="ease-out" fade> <svg>...</svg> </ScrollDraw> // ✓ Uses animation-timeline: view()
These features require the JS engine and trigger the fallback automatically: onProgress, onComplete, waypoints, stagger, morphTo, velocityScale, autoReverse, once, repeat, custom trigger, custom scroll container, speed ≠ 1, spring easing, animated color/width/fill.
<ScrollDraw native={false} easing="spring">
<svg>...</svg>
</ScrollDraw>
// Forces JS engine regardless of browserThe full instance API — pause, resume, seek, replay, destroy — works on both paths.
Zero config
Drop in.
It just works.
Wrap any SVG with <ScrollDraw>. The engine discovers every path, measures total length, and animates stroke-dashoffset as it enters the viewport. No configuration required.
import { ScrollDraw } from 'svg-scroll-draw/react';
export default function Hero() {
return (
<ScrollDraw>
<svg>...</svg>
</ScrollDraw>
);
}Easing
Speed — 1.0×
Easing
Speed — 1.5×
Easing + speed
Natural motion,
your way.
Seven built-in curves — spring, bounce,elastic, and four standard easings — or any custom (t: number) => number function. The speed prop compresses or stretches the draw relative to your scroll distance.
<ScrollDraw
easing="spring"
speed={1.5}
>
<svg>...</svg>
</ScrollDraw>Fade
Draw and
materialise.
Enable fade to simultaneously animate opacity: 0 → 1 as the path draws. Lines seem to emerge from nothing — elegant for technical illustrations and hero graphics.
<ScrollDraw
fade={true}
easing="ease-in-out"
>
<svg>...</svg>
</ScrollDraw>Easing
Speed — 1.0×
Easing
Speed — 0.9×
Auto-discovery
Every path.
Automatically.
Every <path>, <line>, <polyline>, and <polygon> inside the container is discovered, measured, and animated.
No manual selectors. No brittle ID targeting. When your designer updates the SVG, your code doesn't break.
Callback
Know when
it's done.
The onComplete callback fires the moment the path reaches 100%. Use it to chain animations, reveal UI, or fire analytics. Hit Replay below to see it fire again.
<ScrollDraw
onComplete={() => {
console.log('drawn!');
}}
>
<svg>...</svg>
</ScrollDraw>Easing
Speed — 1.0×
Easing
Speed — 0.9×
Auto Reverse
Scroll up,
draw back.
Enable autoReverse and the animation automatically follows scroll direction — drawing forward as you scroll down, erasing as you scroll back up. No manual direction switching needed.
<ScrollDraw autoReverse> <svg>...</svg> </ScrollDraw>
Color Animation
Color that
follows the draw.
Pass a [from, to] tuple to strokeColor and the stroke interpolates between two colors as the path draws. No extra CSS or keyframes — the engine handles it per-frame.
<ScrollDraw
strokeColor={['#ff6b9d', '#ffc900']}
easing="ease-out"
>
<svg>...</svg>
</ScrollDraw>Easing
Speed — 0.8×
Easing
Speed — 0.9×
Width Animation
Hairline thin
to bold.
Pass a [from, to] tuple to strokeWidth and the line grows from a hairline to any thickness as it draws. Combine with strokeColor for dramatic logo reveals.
<ScrollDraw
strokeWidth={[0.5, 6]}
easing="ease-in-out"
>
<svg>...</svg>
</ScrollDraw>Waypoints
Fire callbacks
mid-draw.
waypoints lets you fire callbacks at any progress threshold — reveal UI at 50%, trigger confetti at 100%, or sync multiple animations precisely. They reset on replay().
<ScrollDraw
waypoints={{
0.25: () => revealLabel(),
0.5: () => animateChart(),
0.75: () => showCTA(),
1: () => fireConfetti(),
}}
>
<svg>...</svg>
</ScrollDraw>Easing
Speed — 0.8×
Path Morphing
Shape-shifts
as you scroll.
Pass a morphTo path string and the SVG shape interpolates from its original d to the target as you scroll. Both paths must have the same command count — perfect for logo reveals and icon transitions.
<ScrollDraw
morphTo="M 130 40 L 220 130 L 130 220 L 40 130 Z"
easing="ease-in-out"
>
<svg>
<path d="M 130 40 C 220 40 220 220 130 220
C 40 220 40 40 130 40 Z" />
</svg>
</ScrollDraw>Easing
Speed — 0.9×
Group API
Many SVGs,
one command.
Use scrollDrawGroup to animate multiple SVG containers simultaneously with shared options. Or use scrollDrawSequence to chain them — each one starts only after the previous finishes.
import { scrollDrawGroup, scrollDrawSequence }
from 'svg-scroll-draw/group';
// Animate all three at once
const group = scrollDrawGroup(
['#chart-1', '#chart-2', '#chart-3'],
{ easing: 'ease-out', stagger: 0.1 }
);
// Or chain them in sequence
const seq = scrollDrawSequence(
['#step-1', '#step-2', '#step-3'],
{ easing: 'spring' }
);useScrollDrawProgress
Progress as a
React value.
useScrollDrawProgress exposes scroll progress (0–1) as a plain React number — no SVG required. Drive any animation: colors, counters, layouts, spring physics, or canvas. Same trigger/speed/easing API as ScrollDraw.
import { useScrollDrawProgress }
from 'svg-scroll-draw/react';
const ref = useRef(null);
const progress = useScrollDrawProgress(ref, {
trigger: { start: 'top 80%',
end: 'bottom 20%' },
easing: 'ease-out',
});
// progress is 0 → 1 as you scroll
// drive anything with it ↓
const opacity = progress;
const scale = 0.8 + progress * 0.2;
const color = lerp('#ccc', '#ff90e8', progress);Fill Opacity
Outline draws.
Fill floods in.
Pass fillOpacity={[0, 1]} and the element's fill animates from fully transparent to fully opaque in sync with the stroke draw. A single number sets a static override. No React state, no onComplete hack needed.
<ScrollDraw
easing="ease-in-out"
speed={0.9}
fillOpacity={[0, 1]}
strokeColor={['#d1d5dc', '#ff90e8']}
once
>
<svg>
{/* fill is set on the element — */}
{/* fillOpacity animates 0 → 1 */}
<path fill="#ff90e8" stroke="#ff90e8" d="…" />
</svg>
</ScrollDraw>Clip Mode
Reveal anything.
Not just SVGs.
Set clip to skip stroke-dashoffset entirely and use CSS clip-path instead. Works on images, cards, text blocks, gradients — any HTML or SVG content.
Five directions: left, right, top, bottom, center. All existing options — easing, speed, trigger, once — work as normal.
// Reveal any content — not just SVG paths
<ScrollDraw clip="left" easing="ease-out" once>
<img src="/hero.png" />
</ScrollDraw>
<ScrollDraw clip="center" easing="spring" speed={1.2}>
<div className="card">
<h2>Radial reveal</h2>
<p>Works on any HTML content.</p>
</div>
</ScrollDraw>
// true defaults to 'left'
<ScrollDraw clip easing="ease-in-out">
<YourComponent />
</ScrollDraw>Autoplay
Draw on enter.
No scroll needed.
Set autoplay to run the animation over duration milliseconds the moment the element enters the viewport — perfect for hero sections, loaders, and page transitions where scroll position is irrelevant.
Every option works as normal: easing, stagger, fade, clip, morphTo, callbacks, repeat. Use once to play only the first time the element enters view.
// Draws on viewport enter — no scroll required
<ScrollDraw
autoplay
duration={1200}
easing="ease-out"
stagger={0.12}
once
>
<svg>...</svg>
</ScrollDraw>
// Vanilla JS
scrollDraw('#logo', {
autoplay: true,
duration: 1200,
easing: 'spring',
});Scroll away and back — it replays on re-entry.
Set once to play only once.
Reference
All options.
selectortype:stringdefault:"path, polyline…"CSS selector to target specific child elements.
speedtype:numberdefault:1Scale factor — values above 1 complete the animation faster.
fadetype:booleandefault:falseAnimate opacity 0 → 1 simultaneously while drawing.
easingtype:string | fndefault:"linear"linear · ease-in · ease-out · ease-in-out · spring · or custom (t) => t.
staggertype:numberdefault:0Offset between each path starting. In scroll mode: fraction of the scroll range (0.15 → each path begins 15% of the range after the previous). In autoplay mode: fraction of duration (0.15 → 150ms apart at duration=1000).
directiontype:"forward"|"reverse"default:"forward"forward draws the path in. reverse starts fully drawn and erases as you scroll.
oncetype:booleandefault:falseDraw once and stay drawn — animation does not reverse when scrolling back up.
debugtype:booleandefault:falseRenders a visual overlay showing trigger start/end zones. Dev-only, stripped in production.
axistype:"x" | "y"default:"y"Scroll axis to track. Use "x" for horizontal scroll containers.
scrollContainertype:string | Elementdefault:windowCSS selector or Element for a custom scroll container instead of the window.
autoReversetype:booleandefault:falseAutomatically reverse the animation when the user scrolls back up.
delaytype:numberdefault:0Milliseconds to wait before the engine starts observing — useful for page-load sequences.
strokeColortype:string | [string,string]default:—Static color override or [from, to] tuple to animate stroke color as the path draws.
strokeWidthtype:number | [number,number]default:—Static width override or [from, to] tuple to animate stroke width as the path draws.
fillOpacitytype:number | [number,number]default:—Static fill-opacity override or [from, to] tuple to animate fill opacity in sync with the stroke draw. Use [0, 1] to flood fill as the outline traces itself.
cliptype:boolean | stringdefault:falseReveal using CSS clip-path instead of stroke-dashoffset. Works on any HTML/SVG content. Values: 'left' (default), 'right', 'top', 'bottom', 'center' (radial). All easing/speed/trigger options apply.
waypointstype:Record<number, fn>default:—Fire callbacks at specific progress thresholds (0–1). e.g. { 0.5: () => doSomething() }. Resets on replay().
trigger.starttype:stringdefault:"top bottom"When animation begins. Accepts anchor strings ("top bottom") or viewport percentages ("20%").
trigger.endtype:stringdefault:"bottom top"When animation ends. Accepts anchor strings or viewport percentages.
velocityScaletype:boolean | numberdefault:falseScale draw speed by scroll velocity — faster scrolling draws faster. Pass a number to set sensitivity.
thresholdtype:numberdefault:0IntersectionObserver threshold — how much of the element must be visible before animating.
rootMargintype:stringdefault:"0px"IntersectionObserver rootMargin — expand or shrink the trigger zone.
repeattype:number | "infinite"default:0Repeat the animation N times. Use "infinite" to loop forever.
repeatDelaytype:numberdefault:0Milliseconds to wait between repeats.
morphTotype:stringdefault:—SVG path `d` value to morph toward as the animation progresses.
onStarttype:() => voiddefault:—Fires once on the first frame the animation begins drawing.
onProgresstype:(n: number) => voiddefault:—Called every animation frame with current draw alpha (0–1).
onCompletetype:() => voiddefault:—Fires once when all paths reach 100% draw progress.
autoplaytype:booleandefault:falseDraw on viewport enter instead of scroll. Replays each time the element enters view; use once to play only the first time.
durationtype:numberdefault:1000Duration in milliseconds for the autoplay animation. Only used when autoplay: true.
nativetype:booleandefault:trueUse the browser's native CSS scroll-driven animation (animation-timeline: view()) when eligible. Falls back to the JS engine automatically. Set false to always use the JS engine.
useScrollDrawProgresstype:hookdefault:—React hook — returns scroll progress (0–1) for any element. Same trigger/speed/easing options as ScrollDraw. No SVG required.
Quickstart
Works everywhere
you do.
import { ScrollDraw } from 'svg-scroll-draw/react';
export default function Hero() {
return (
<ScrollDraw easing="ease-out" speed={1.2}>
<svg>...</svg>
</ScrollDraw>
);
}Beyond SVG.
Animate everything.
scrollAnimatev2.0.0css · any element
Animate opacity, transform, color — any CSS property on any element. Direct replacement for gsap.to + ScrollTrigger.
scrollAnimate('#card', {
props: {
opacity: [0, 1],
transform: [
'translateY(40px)',
'translateY(0)',
],
},
easing: 'ease-out',
once: true,
});scrollCounterv2.0.0numbers · format fn
Animate any number into view. Custom format function, decimals, easing. One call for an entire stats section.
scrollCounter('#revenue', {
to: 1_250_000,
format: n =>
'$' + Math.round(n)
.toLocaleString(),
easing: 'ease-out',
once: true,
});scrollTextv2.1.0chars · words · lines
Split text and stagger-animate each piece on scroll. Free GSAP SplitText replacement — no Club GreenSock subscription.
scrollText('#headline', {
split: 'words',
stagger: 0.05,
from: {
opacity: 0,
y: 24,
},
once: true,
});scrollVideov2.1.0currentTime · scrub
Tie <video> playback to scroll. The Apple/Stripe product page pattern — ships free.
import { scrollVideo }
from 'svg-scroll-draw/video';
scrollVideo('#hero-video', {
trigger: {
start: 'top top',
end: 'bottom top',
},
});scrollParallaxv2.0.0speed · depth
Move any element at a different rate than scroll. Speed multiplier, reverse direction. One line.
// 40% of scroll speed
scrollParallax('#bg', {
speed: 0.4,
});
// Opposite direction
scrollParallax('#badge', {
speed: -0.3,
});devtoolsv2.6.0overlay · dev-only
Visual panel showing every active animation's trigger window, progress, and type. Zero production bytes.
import { devtools }
from 'svg-scroll-draw/devtools';
// Cmd+Shift+S to toggle
devtools.enable();scrollPinv2.7.0pin · sticky · fixed
Pin any element at a viewport position while the page scrolls past it. Wrapper-based — no layout shift. Full lifecycle callbacks.
import { scrollPin }
from 'svg-scroll-draw/pin';
scrollPin('#panel', {
pinDistance: 800,
onEnter: () => activate(),
onLeave: () => deactivate(),
onEnterBack: () => activate(),
});scrollSnapv2.7.0snap · sections · js
JS-powered section snapping with custom easing, configurable threshold, and onSnap callback. Vertical and horizontal.
import { scrollSnap }
from 'svg-scroll-draw/snap';
scrollSnap('.section', {
duration: 600,
easing: 'ease-in-out',
threshold: 0.3,
onSnap: (i) =>
console.log('section', i),
});onEnter / onLeavev2.7.0callbacks · lifecycle
Fire code when scroll enters or exits any trigger zone — in either direction. Works on scrollAnimate and scrollDraw.
scrollAnimate('#section', {
props: { opacity: [0.3, 1] },
onEnter: () => activate(),
onLeave: () => deactivate(),
onEnterBack: () => activate(),
onLeaveBack: () => deactivate(),
});scrollProgressv2.9.0css variable · calc()
Expose scroll progress as --scroll-progress CSS custom property. Drive opacity, transforms, backgrounds — anything — directly from CSS calc() with zero per-frame JS.
import { scrollProgress }
from 'svg-scroll-draw/progress';
scrollProgress('#hero');
// Then in CSS:
// #hero {
// opacity: calc(
// var(--scroll-progress-eased)
// );
// }scrollHorizontalv2.9.0horizontal · apple-style
Drive horizontal translateX from vertical scroll. The Apple / Stripe "scroll sideways" pattern. One call — you handle the sticky CSS.
import { scrollHorizontal }
from 'svg-scroll-draw/horizontal';
scrollHorizontal('.track', {
distance: window.innerWidth * 3,
easing: 'linear',
});scrollRevealv2.8.0reveal · presets · stagger
One-line replacement for AOS and ScrollReveal.js. 7 presets, stagger, custom easing. No data attributes — pure JS.
import { scrollReveal }
from 'svg-scroll-draw/reveal';
// Fade up (default)
scrollReveal('.card');
// Custom from state
scrollReveal('.feature', {
from: { opacity: 0, y: 40, scale: 0.95 },
stagger: 0.1,
});Animate any CSS property.
Try it live.
Effect
opacity + translateY — the most common scroll entrance
Easing
Progress
0%import { scrollAnimate } from 'svg-scroll-draw';
scrollAnimate('#element', {
props: {
opacity: ['0', '1'],
transform: ['translateY(36px)', 'translateY(0px)'],
},
easing: 'ease-out',
once: true,
});Drag the scrubber · switch effects · change easing
Split text, stagger animate.
Free GSAP SplitText replacement.
Split by
Animation preset
opacity + translateY — the default entrance
Stagger
0.05Progress
0%import { scrollText } from 'svg-scroll-draw/text';
scrollText('#headline', {
split: 'words',
stagger: 0.05,
from: {
opacity: 0,
y: 28,
},
once: true,
});Scroll-driven text animation. Every word, every letter, every line.
Drag scrubber · switch split mode · change preset · adjust stagger
Compare