Open source · MIT · Zero dependencies

ANIMATE SVG
PATHS AS YOU SCROLL.

The definitive modern library. ~3 KB gzipped. Works in React, Next.js, and vanilla JS.

$npm i svg-scroll-draw
See it in action ↓
~3 KBgzipped
0dependencies
SSRsafe
56tests ✓
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~3 KB GzippedZero Dependencies56 Tests PassingReact + 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~3 KB GzippedZero Dependencies56 Tests PassingReact + 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

~3 KB.
Not 40 KB.

svg-scroll-draw✓ yours
~3 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.

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>
  );
}

Easing

Speed — 1.0×

0.3× slow3× fast
02

Easing

Speed — 1.5×

0.3× slow3× fast

Easing + speed

Natural motion,
your way.

Five built-in curves — including a spring that overshoots and settles — 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

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

Normalized scroll-progress offset between each path starting. e.g. 0.15 → each path begins 15% of the range after the previous.

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.

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>
  );
}

THE MODERN
STANDARD FOR
SCROLL-DRAWN SVG.