The setup
You know the pattern. You have an SVG — a logo, a path, a hand-drawn illustration — and you want it to draw itself as the user scrolls. It looks incredible. Every design agency uses it. Every portfolio has it.
So you reach for GSAP DrawSVG.
Then you see the pricing page.
GSAP DrawSVG requires a Club GreenSock license for commercial use. That's fine if you're at an agency billing $200/hr. It's annoying if you're shipping a SaaS or an open-source project. And either way, you just imported ~40 KB of runtime for one visual effect.
Framer Motion does scroll-triggered SVG animation too. It's ~35 KB, React-only, and your Vue teammates will look at you funny.
I got tired of the options, so I built the missing one.
svg-scroll-draw — what it is
npm i svg-scroll-draw
~4.4 KB gzipped. Zero dependencies. MIT license.
Works in React, Next.js, Vue 3, Svelte, Solid.js, Angular, Nuxt, Astro, Web Component, and vanilla JS — same library, nine thin adapters.
The simplest possible usage:
// React
import { ScrollDraw } from 'svg-scroll-draw/react';
export function Hero() {
return (
<ScrollDraw>
<svg viewBox="0 0 200 100" fill="none">
<path d="M10 50 Q100 10 190 50" stroke="black" strokeWidth="2" />
</svg>
</ScrollDraw>
);
}
That's it. The engine discovers every path, line, rect, circle, polyline, and polygon inside the container, measures total stroke length, and animates stroke-dashoffset as the element enters the viewport. No configuration required.
The part that makes it different: native CSS
Here's what I didn't expect when building this: for the common case, you don't need JavaScript at all.
When the browser supports animation-timeline: view() (Chrome, Edge, Firefox — about 75% of global traffic today), svg-scroll-draw hands the animation to the compositor entirely. Zero per-frame JS. No scroll listeners. No requestAnimationFrame. The GPU does it.
// This runs on the compositor in supporting browsers —
// no scroll event, no rAF, no JS per frame
<ScrollDraw easing="ease-out" fade>
<svg>...</svg>
</ScrollDraw>
The library detects support at runtime and falls back to the full JS engine automatically. You never change your code.
What triggers the JS fallback?
Any feature CSS can't express declaratively: onProgress / onComplete callbacks, stagger, morphTo, velocityScale, autoReverse, once, repeat, custom scroll containers, speed ≠ 1, spring easing, or animated strokeColor / strokeWidth / fillOpacity.
The escape hatch if you need it:
// Force the JS engine regardless
<ScrollDraw native={false} easing="spring">
<svg>...</svg>
</ScrollDraw>
The full instance API — pause(), resume(), seek(), replay(), destroy() — works identically on both paths.
It's not just stroke-dashoffset
I've been thinking about scroll-driven SVG animation for a while, and there are more patterns than just "draw a line":
Stroke color animation — the stroke interpolates between two colors as the path draws:
<ScrollDraw strokeColor={['#ff6b9d', '#ffc900']} easing="ease-out">
<svg>...</svg>
</ScrollDraw>
Fill opacity flood — the fill appears as the outline traces itself. Useful for logo reveals where the shape "fills in" after the stroke completes:
<ScrollDraw fillOpacity={[0, 1]} easing="ease-out">
<svg>
<path d="..." stroke="#ff90e8" fill="#ff90e8" />
</svg>
</ScrollDraw>
Path morphing — the shape interpolates from its original d to a target on scroll:
<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" stroke="black" />
</svg>
</ScrollDraw>
Clip-path reveal — reveal any content (not just SVGs — images, divs, text) using CSS clip-path instead of stroke animation:
// Works on images, headings, anything
<ScrollDraw clip="left" speed={0.8}>
<img src="/hero.jpg" alt="..." />
</ScrollDraw>
Stagger — multiple paths draw in sequence, each starting after the previous:
<ScrollDraw stagger={0.1} easing="ease-out">
<svg>
{/* chart bars, each draws in sequence */}
<rect ... />
<rect ... />
<rect ... />
</svg>
</ScrollDraw>
Velocity scale — draw speed scales with how fast the user is scrolling:
<ScrollDraw velocityScale={1.5}>
<svg>...</svg>
</ScrollDraw>
Framework usage
Vue 3:
<template>
<ScrollDraw easing="ease-out" :speed="1.2" fade once>
<svg viewBox="0 0 200 100" fill="none">
<path d="M10 50 Q100 10 190 50" stroke="black" stroke-width="2" />
</svg>
</ScrollDraw>
</template>
<script setup>
import { ScrollDraw } from 'svg-scroll-draw/vue';
</script>
Svelte:
<script>
import { scrollDraw } from 'svg-scroll-draw/svelte';
</script>
<div use:scrollDraw={{ easing: 'spring', fade: true, once: true }}>
<svg viewBox="0 0 200 100" fill="none">
<path d="M10 50 Q100 10 190 50" stroke="black" stroke-width="2" />
</svg>
</div>
Vanilla JS:
import { scrollDraw } from 'svg-scroll-draw';
const instance = scrollDraw('#hero-svg', {
easing: 'ease-out',
speed: 1.2,
fade: true,
once: true,
onComplete: () => console.log('all paths drawn!'),
});
// Full imperative control
instance.pause();
instance.resume();
instance.seek(0.5); // jump to 50%
instance.replay();
instance.destroy(); // cleanup on unmount
Astro:
<div
data-scroll-draw
data-scroll-draw-options='{"easing":"ease-out","fade":true,"once":true}'
>
<svg viewBox="0 0 200 100" fill="none">
<path d="M10 50 Q100 10 190 50" stroke="black" stroke-width="2" />
</svg>
</div>
<script>
import { initScrollDraw } from 'svg-scroll-draw/astro';
initScrollDraw();
</script>
Bundle size comparison (minified + gzipped)
| Library | Size | License | Frameworks |
|---|---|---|---|
| svg-scroll-draw | ~4.4 KB | MIT (free) | React, Vue, Svelte, Solid, Angular, Nuxt, Astro, Web Component, Vanilla |
| Framer Motion | ~35 KB | MIT | React only |
| GSAP DrawSVG | ~40 KB | Commercial license required | Any (with adapter) |
Under the hood — how the JS engine works
For the curious: the core engine uses IntersectionObserver to start a requestAnimationFrame loop when the element enters the viewport, and stops it when the element exits. This avoids scroll listeners entirely and keeps idle cost near zero.
The progress value is derived from getBoundingClientRect() relative to the viewport, clamped to [0, 1], and mapped through the easing function. stroke-dasharray is set once (to the total path length), and only stroke-dashoffset changes per frame — a cheap, compositable property.
The prefers-reduced-motion check is handled in the engine: if the user has reduced motion enabled, the draw completes immediately at full progress without animation.
Try it
- Live playground: https://svg-scroll-draw.vercel.app/playground — paste any SVG, tweak options, get a shareable link
- Examples: https://svg-scroll-draw.vercel.app/examples — 10 production-ready demos (logo reveal, revenue chart, delivery route, API architecture diagram, etc.)
- Docs: https://svg-scroll-draw.vercel.app/docs
-
npm:
npm i svg-scroll-draw - GitHub: https://github.com/DhruvilChauahan0210/ink-scroll
221 tests passing. 7 suites. MIT.
If you've been reaching for GSAP or Framer just to draw an SVG on scroll, give this a shot. The playground makes it easy to test with your own SVG before adding it to your project.
Top comments (0)