DEV Community

Dhruvil Chauhan
Dhruvil Chauhan

Posted on

I replaced GSAP DrawSVG with 4.4 KB (and it uses native CSS where it can)

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
Enter fullscreen mode Exit fullscreen mode

~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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Velocity scale — draw speed scales with how fast the user is scrolling:

<ScrollDraw velocityScale={1.5}>
  <svg>...</svg>
</ScrollDraw>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

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)