Open source · MIT · Zero dependencies

ANIMATE SVG
PATHS AS YOU SCROLL.

The scroll animation platform. ~9 KB gzipped. Works in React, Vue 3, Svelte, Solid, and more.

$npm i svg-scroll-draw
See it in action ↓
~9 KBgzipped
0dependencies
SSRsafe
423tests ✓
scroll

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.

showcase.tsx
<ScrollDraw
  selector=".ink-ribbon"
  easing="ease-out"
  speed={1.5}
>
  <svg>{/* fountain pen */}</svg>
</ScrollDraw>
Scroll to draw the ink ribbon
Container AutomationSSR SaferequestAnimationFrameIntersectionObserver~9 KB GzippedZero Dependencies423 Tests PassingscrollAnimatescrollCounterscrollTextReact + Next.jsVue 3SvelteVanilla JSWeb ComponentReduced MotionSpring EasingOnce ModeDebug OverlayHorizontal ScrollReplay APIColor AnimationAuto ReverseWaypointsSolidJSAngularAstroNuxtPath MorphingTimeline APIcreateSpringCSS Custom PropertyGroup APISequence APIPause · Resume · SeekVelocity ScaleRepeatuseScrollDrawProgressFill OpacityClip ModeContainer AutomationSSR SaferequestAnimationFrameIntersectionObserver~9 KB GzippedZero Dependencies423 Tests PassingscrollAnimatescrollCounterscrollTextReact + Next.jsVue 3SvelteVanilla JSWeb ComponentReduced MotionSpring EasingOnce ModeDebug OverlayHorizontal ScrollReplay APIColor AnimationAuto ReverseWaypointsSolidJSAngularAstroNuxtPath MorphingTimeline APIcreateSpringCSS Custom PropertyGroup APISequence APIPause · Resume · SeekVelocity ScaleRepeatuseScrollDrawProgressFill OpacityClip Mode

The problem

Every existing tool
is broken.

GSAP DrawSVG
40KB+Paid license

Overkill for a single effect — and requires a Club GreenSock subscription for commercial use.

Framer Motion
35KB+React only

Locks you into one ecosystem and adds 35KB of runtime overhead just to draw a line.

scroll-svg
~2KBAbandoned

Requires manually targeting individual path IDs. Crashes in Next.js with window is not defined.

Bundle size

~9 KB.
Not 40 KB.

svg-scroll-draw✓ yours
~9 KB gzip
Framer Motion
~35 KB gzip
GSAP DrawSVG
~40 KB gzip

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.

Native CSS fast path

Runs on the compositor when you use: a default or named trigger, a named easing, optional fade, forward or reverse direction.

native.tsx
<ScrollDraw easing="ease-out" fade>
  <svg>...</svg>
</ScrollDraw>
// ✓ Uses animation-timeline: view()
JS engine (auto fallback)

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.

js-engine.tsx
<ScrollDraw native={false} easing="spring">
  <svg>...</svg>
</ScrollDraw>
// Forces JS engine regardless of browser

The full instance API — pause, resume, seek, replay, destroy — works on both paths.

01

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.

index.tsx
import { ScrollDraw } from 'svg-scroll-draw/react';

export default function Hero() {
  return (
    <ScrollDraw>
      <svg>...</svg>
    </ScrollDraw>
  );
}
your browser:

Easing

Speed — 1.0×

0.3× slow3× fast
02

Easing

Speed — 1.5×

0.3× slow3× fast

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.

index.tsx
<ScrollDraw
  easing="spring"
  speed={1.5}
>
  <svg>...</svg>
</ScrollDraw>
03

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.

index.tsx
<ScrollDraw
  fade={true}
  easing="ease-in-out"
>
  <svg>...</svg>
</ScrollDraw>

Easing

Speed — 1.0×

0.3× slow3× fast
04

Easing

Speed — 0.9×

0.3× slow3× fast

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.

05

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.

index.tsx
<ScrollDraw
  onComplete={() => {
    console.log('drawn!');
  }}
>
  <svg>...</svg>
</ScrollDraw>
onComplete fired ✓

Easing

Speed — 1.0×

0.3× slow3× fast
06

Easing

Speed — 0.9×

0.3× slow3× fast

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.

index.tsx
<ScrollDraw autoReverse>
  <svg>...</svg>
</ScrollDraw>
07

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.

index.tsx
<ScrollDraw
  strokeColor={['#ff6b9d', '#ffc900']}
  easing="ease-out"
>
  <svg>...</svg>
</ScrollDraw>

Easing

Speed — 0.8×

0.3× slow3× fast
08

Easing

Speed — 0.9×

0.3× slow3× fast

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.

index.tsx
<ScrollDraw
  strokeWidth={[0.5, 6]}
  easing="ease-in-out"
>
  <svg>...</svg>
</ScrollDraw>
09

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().

25%
50%
75%
100%
index.tsx
<ScrollDraw
  waypoints={{
    0.25: () => revealLabel(),
    0.5:  () => animateChart(),
    0.75: () => showCTA(),
    1:    () => fireConfetti(),
  }}
>
  <svg>...</svg>
</ScrollDraw>
25%50%75%100%

Easing

Speed — 0.8×

0.3× slow3× fast
10

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.

index.tsx
<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>
morphs to →

Easing

Speed — 0.9×

0.3× slow3× fast
11

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.

main.js
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' }
);
12

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.

Hero.tsx
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);
0%
scroll progress0.000
0.00
opacity
0.80
scale
8.0
blur (px)
13

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.

Hero.tsx
<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>
fillOpacity accepts the same shapes as strokeWidth: a number for a static override, or a [from, to] tuple to interpolate.
14

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.

Hero.tsx
// 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>
vs stroke-dashoffset: dashoffset traces the path shape — perfect for drawing lines. Clip reveals a rectangular or circular window — perfect for images, cards, and content blocks.
← leftWipes in from left to right
right →Wipes in from right to left
↑ topDrops in from top to bottom
↓ bottomRises in from bottom to top
◎ centerRadial reveal from center
15

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.

Hero.tsx
// 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',
});
1200ms
0.12

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:1

Scale factor — values above 1 complete the animation faster.

fadetype:booleandefault:false

Animate 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:0

Offset 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:false

Draw once and stay drawn — animation does not reverse when scrolling back up.

debugtype:booleandefault:false

Renders 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:window

CSS selector or Element for a custom scroll container instead of the window.

autoReversetype:booleandefault:false

Automatically reverse the animation when the user scrolls back up.

delaytype:numberdefault:0

Milliseconds 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:false

Reveal 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:false

Scale draw speed by scroll velocity — faster scrolling draws faster. Pass a number to set sensitivity.

thresholdtype:numberdefault:0

IntersectionObserver 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:0

Repeat the animation N times. Use "infinite" to loop forever.

repeatDelaytype:numberdefault:0

Milliseconds 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:false

Draw on viewport enter instead of scroll. Replays each time the element enters view; use once to play only the first time.

durationtype:numberdefault:1000

Duration in milliseconds for the autoplay animation. Only used when autoplay: true.

nativetype:booleandefault:true

Use 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.

Hero.tsx
import { ScrollDraw } from 'svg-scroll-draw/react';

export default function Hero() {
  return (
    <ScrollDraw easing="ease-out" speed={1.2}>
      <svg>...</svg>
    </ScrollDraw>
  );
}
New in v2.0 – v2.2

Beyond SVG.
Animate everything.

See live examples →
scrollAnimatev2.0.0

css · 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.0

numbers · 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.0

chars · 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.0

currentTime · 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.0

speed · 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.0

overlay · 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.0

pin · 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.0

snap · 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.0

callbacks · 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.0

css 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.0

horizontal · 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.0

reveal · 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,
});
v2.0.0 · scrollAnimate · interactive

Animate any CSS property.
Try it live.

Effect

opacity + translateY — the most common scroll entrance

Easing

Progress

0%
from stateto state
scroll-animate.js
import { scrollAnimate } from 'svg-scroll-draw';

scrollAnimate('#element', {
  props: {
    opacity: ['0', '1'],
    transform: ['translateY(36px)', 'translateY(0px)'],
  },
  easing: 'ease-out',
  once:   true,
});
scrollAnimate
Any CSS property.
Any element.
opacitytransformcolorbackground
npm i svg-scroll-draw

Drag the scrubber · switch effects · change easing

v2.2.0 · scrollText · interactive

Split text, stagger animate.
Free GSAP SplitText replacement.

Split by

Animation preset

opacity + translateY — the default entrance

Stagger

0.05
0 (all at once)0.10 (slowest cascade)

Progress

0%
hiddenfully revealed
scroll-text.js
import { scrollText } from 'svg-scroll-draw/text';

scrollText('#headline', {
  split:   'words',
  stagger: 0.05,
  from: {
    opacity: 0,
    y: 28,
  },
  once: true,
});
scrollText

Scroll-driven text animation. Every word, every letter, every line.

split:wordsstagger:0.05

Drag scrubber · switch split mode · change preset · adjust stagger

THE MODERN
STANDARD FOR
SCROLL-DRAWN SVG.