InstallationQuick StartCoreTriggerVisualCallbacksAdvancedMethodsReactVue 3SvelteSolid.jsAngularNuxtAstroWeb ComponentVanilla JSGroup APISequence APIuseScrollDrawProgresscreateSpringscrollDrawTimelineCSS Custom PropTypes

Getting Started

Installation

Install via your package manager of choice, or drop in a CDN script tag — no build step required.

$ npm i svg-scroll-draw
$ pnpm add svg-scroll-draw
$ yarn add svg-scroll-draw
$ bun add svg-scroll-draw

CDN

index.html
<script src="https://unpkg.com/svg-scroll-draw/dist/cdn/svg-scroll-draw.global.js"></script>

Exposes window.SvgScrollDraw globally. Use the <scroll-draw> custom element or call SvgScrollDraw.scrollDraw() directly.

Getting Started

Quick Start

Point scrollDraw() at any element containing an SVG. All path, line, rect, and circle elements inside are animated automatically.

main.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!'),
});

// Control playback later
instance.pause();
instance.resume();
instance.seek(0.5);   // jump to 50%
instance.replay();
instance.destroy();   // cleanup on unmount
Note: SVG elements must have a stroke attribute and fill="none". In dev mode, a warning is logged if either condition is violated.

Options

Core Options

selectorstringdefault: 'path, polyline, line, polygon, rect, circle'

CSS selector for elements to animate inside the container. Override to target specific paths by class or ID.

speednumberdefault: 1

Animation speed multiplier. Values above 1 complete the draw over a shorter scroll distance. Values below 1 slow it down.

easing'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'spring' | (t: number) => numberdefault: 'linear'

Easing curve applied to draw progress. Pass a custom function for full control — receives and returns a 0–1 value.

direction'forward' | 'reverse'default: 'forward'

'forward' draws the path as you scroll in. 'reverse' starts fully drawn and erases it — useful for "write then erase" effects.

staggernumberdefault: 0

Delay between each path starting, as a fraction of the total scroll range. 0.1 means each path starts 10% of the range after the previous one.

oncebooleandefault: false

Lock draw progress at its maximum once reached. Scrolling back up will not erase the paths.

Options

Trigger

Triggers define when the animation starts and ends relative to the scroll position. Both use an "element-anchor viewport-anchor" string format, or a percentage like "20%".

trigger.startstringdefault: 'top bottom'

When the animation begins. 'top bottom' means "when the top of the element hits the bottom of the viewport." Valid anchors: top | center | bottom.

trigger.endstringdefault: 'bottom top'

When the animation completes. 'bottom top' means "when the bottom of the element reaches the top of the viewport."

triggers.js
// Default — animates across the full scroll-through
scrollDraw('#svg', { trigger: { start: 'top bottom', end: 'bottom top' } });

// Tighter window — starts later, finishes earlier
scrollDraw('#svg', { trigger: { start: 'top 60%', end: 'bottom 40%' } });

// Percentage shorthand
scrollDraw('#svg', { trigger: { start: '20%', end: '80%' } });

Options

Visual Effects

fadebooleandefault: false

Fade the element opacity from 0→1 in sync with the draw. Combine with direction: 'reverse' to fade out while erasing.

strokeColorstring | [string, string]

Static stroke color override, or interpolate between two colors as the path draws. Example: strokeColor=['#ff90e8', '#ffc900'].

strokeWidthnumber | [number, number]

Static stroke width, or animate from one width to another as drawing progresses.

fillOpacitynumber | [number, number]

Animate fill opacity. Pass [0, 1] to flood-fill the shape in sync with the stroke draw — no callbacks or React state needed.

clipboolean | 'left' | 'right' | 'top' | 'bottom' | 'center'

Reveal the container using CSS clip-path instead of stroke animation. Works on any content — images, text, divs. true defaults to 'left'.

morphTostring

Target SVG d attribute to morph toward as the animation progresses. Source and target paths must have compatible structures — same number of commands and coordinate pairs.

visual-effects.js
// Color interpolation
scrollDraw('#svg', { strokeColor: ['#ff90e8', '#ffc900'] });

// Fill flood in sync with draw (logo reveal)
scrollDraw('#logo', { fillOpacity: [0, 1], easing: 'ease-out' });

// Clip-path reveal on an image or div
scrollDraw('#image-wrapper', { clip: 'left', speed: 0.8 });

// Path morphing
scrollDraw('#shape', { morphTo: 'M10 80 Q50 10 90 80', easing: 'spring' });

Options

Callbacks & Waypoints

onProgress(alpha: number) => void

Called on every animation frame with the current draw progress (0–1). Use it to drive any side effect in sync with the SVG draw.

onStart() => void

Fires once when the animation begins — the first frame where progress > 0.

onComplete() => void

Fires once when all paths have reached full draw progress (alpha = 1).

waypointsRecord<number, () => void>

Fire callbacks at specific progress thresholds. Keys are 0–1 values. Each fires once per cycle and resets on replay().

callbacks.js
scrollDraw('#svg', {
  onStart:    () => console.log('started'),
  onProgress: (p) => (label.style.opacity = p),
  onComplete: () => badge.classList.add('visible'),

  waypoints: {
    0.25: () => console.log('25% drawn'),
    0.5:  () => triggerConfetti(),
    1.0:  () => showNextSection(),
  },
});

Options

Advanced Options

autoReversebooleandefault: false

Automatically reverse the animation direction when scrolling back up. Overrides direction.

axis'x' | 'y'default: 'y'

Scroll axis to track. Use 'x' for horizontally-scrolling containers.

scrollContainerstring | Element

CSS selector or Element reference for a custom scroll container. Defaults to window.

delaynumberdefault: 0

Milliseconds to wait before the engine starts observing. Useful for staggering multiple instances on initial page load.

velocityScaleboolean | numberdefault: false

Scale animation speed by scroll velocity — faster scrolling draws faster. Pass a number to control sensitivity (default is 1).

repeatnumber | 'infinite'default: 0

Repeat the animation N times after completion. Use 'infinite' to loop forever.

repeatDelaynumberdefault: 0

Milliseconds to wait between animation repeats.

debugbooleandefault: false

Renders a visual overlay showing the start and end trigger zones. Stripped in production — dev only.

thresholdnumberdefault: 0

IntersectionObserver threshold (0–1). Controls what percentage of the element must be visible before the rAF loop activates.

rootMarginstringdefault: '0px'

IntersectionObserver rootMargin. Adjusts the effective bounding box of the viewport for intersection detection.

Instance

Instance Methods

scrollDraw() returns a ScrollDrawInstance with full playback control.

destroy()() => void

Disconnects the IntersectionObserver, cancels the rAF loop, and removes all event listeners. Always call this on component unmount.

replay()() => void

Resets to initial state and replays from the beginning. Clears the once lock and waypoint history.

pause()() => void

Pauses the rAF loop at the current progress. Scroll position is still tracked.

resume()() => void

Resumes a paused animation from where it stopped.

seek(progress)(progress: number) => void

Jump to a specific progress value (0–1) and pause. Useful for building scrubber controls.

getProgress()() => number

Returns the current draw progress as a number between 0 and 1.

instance-control.js
const instance = scrollDraw('#svg', { easing: 'spring' });

// Scrubber slider
slider.addEventListener('input', (e) => {
  instance.seek(e.target.value / 100);
});

// Pause on hover
svg.addEventListener('mouseenter', () => instance.pause());
svg.addEventListener('mouseleave', () => instance.resume());

// Cleanup
window.addEventListener('unload', () => instance.destroy());

Frameworks

React

The React wrapper is a drop-in component that handles lifecycle automatically. All options are passed as props.

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

export function Hero() {
  return (
    <ScrollDraw
      easing="ease-out"
      speed={1.2}
      fade
      once
      stagger={0.1}
      trigger={{ start: 'top 80%', end: 'center 20%' }}
      onComplete={() => console.log('done!')}
    >
      <svg viewBox="0 0 200 100" fill="none">
        <path d="M10 50 Q100 10 190 50" stroke="black" strokeWidth="2" />
        <circle cx="100" cy="50" r="30" stroke="black" strokeWidth="2" />
      </svg>
    </ScrollDraw>
  );
}
Note: The ScrollDraw component is SSR-safe — it uses useEffect internally and works in Next.js App Router without a 'use client' wrapper on the consumer side.

Frameworks

Vue 3

Two options: a <ScrollDraw> component, or the useScrollDraw composable for custom wrappers.

Component

Hero.vue
<script setup>
import { ScrollDraw } from 'svg-scroll-draw/vue';
</script>

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

Composable

useHeroAnim.ts
import { useScrollDraw } from 'svg-scroll-draw/vue';

// Returns a ref — attach it to your container element
const containerRef = useScrollDraw({ easing: 'spring', speed: 0.9, once: true });

Frameworks

Svelte

A Svelte action — apply it to any container with use:scrollDraw. Options update reactively when props change.

Hero.svelte
<script>
  import { scrollDraw, createScrollDraw } from 'svg-scroll-draw/svelte';

  // For replay/pause control, use createScrollDraw
  const { action, getInstance } = createScrollDraw({ easing: 'ease-out', speed: 1.2 });
</script>

<!-- Simple action -->
<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>

<!-- With instance control -->
<div use:action>
  <svg>...</svg>
</div>
<button on:click={() => getInstance()?.replay()}>Replay</button>

Frameworks

Solid.js

A ref-setter hook — pass it to any container element via the ref prop.

Hero.tsx
import { useScrollDraw, createScrollDraw } from 'svg-scroll-draw/solid';

// Simple hook
function Hero() {
  const ref = useScrollDraw({ easing: 'ease-out', fade: true, once: true });
  return (
    <div ref={ref}>
      <svg viewBox="0 0 200 100" fill="none">
        <path d="M10 50 Q100 10 190 50" stroke="black" stroke-width="2" />
      </svg>
    </div>
  );
}

// With instance control
function HeroWithReplay() {
  const { ref, getInstance } = createScrollDraw({ easing: 'spring' });
  return (
    <>
      <div ref={ref}><svg>...</svg></div>
      <button onClick={() => getInstance()?.replay()}>Replay</button>
    </>
  );
}

Frameworks

Angular

A ScrollDrawRef class integrates with Angular's component lifecycle — no peer dependency on @angular/core is required in the library.

hero.component.ts
import { Component, ViewChild, ElementRef, AfterViewInit, OnDestroy } from '@angular/core';
import { ScrollDrawRef } from 'svg-scroll-draw/angular';

@Component({
  selector: 'app-hero',
  template: `
    <div #container>
      <svg viewBox="0 0 200 100" fill="none">
        <path d="M10 50 Q100 10 190 50" stroke="black" stroke-width="2" />
      </svg>
    </div>
    <button (click)="replay()">Replay</button>
  `,
})
export class HeroComponent implements AfterViewInit, OnDestroy {
  @ViewChild('container') containerRef!: ElementRef<HTMLElement>;
  private draw = new ScrollDrawRef();

  ngAfterViewInit() {
    this.draw.init(this.containerRef.nativeElement, {
      easing: 'ease-out',
      speed:  1.2,
      fade:   true,
      once:   true,
    });
  }

  replay()      { this.draw.replay(); }
  ngOnDestroy() { this.draw.destroy(); }
}

Frameworks

Nuxt

Re-exports the Vue composable and component, plus a plugin factory for global registration.

Per-component import (recommended)

pages/index.vue
<script setup>
import { ScrollDraw } from 'svg-scroll-draw/nuxt';
</script>

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

Global registration via Nuxt plugin

plugins/svg-scroll-draw.ts
import { createScrollDrawPlugin } from 'svg-scroll-draw/nuxt';

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(createScrollDrawPlugin());
});

// <ScrollDraw> is now available globally — no per-component imports needed

Frameworks

Astro

initScrollDraw() auto-initialises all elements with a data-scroll-draw attribute. Options are read from a JSON attribute.

src/pages/index.astro
---
// no server-side code needed
---

<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(); // scans the whole document for [data-scroll-draw]
</script>
Note: Pass a root element to initScrollDraw(root) to scope initialisation to a specific subtree.

Frameworks

Web Component

A <scroll-draw> custom element that works in plain HTML, any framework, or WordPress. Bundled into the CDN script — no additional imports needed.

index.html
<script src="https://unpkg.com/svg-scroll-draw/dist/cdn/svg-scroll-draw.global.js"></script>

<scroll-draw 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>
</scroll-draw>

String and number options map directly to HTML attributes. Boolean options like fade and once are presence-based (add the attribute to enable).

Frameworks

Vanilla JS

main.js
import { scrollDraw } from 'svg-scroll-draw';

// Single element
const logo = scrollDraw('#logo', { easing: 'spring', once: true });

// Multiple elements with different configs
const chart  = scrollDraw('#chart',  { stagger: 0.08, speed: 0.8 });
const banner = scrollDraw('#banner', { clip: 'left', speed: 1.5 });

// Cleanup all on unload
window.addEventListener('unload', () => {
  [logo, chart, banner].forEach((i) => i.destroy());
});

Multi-element

Group API

Animate multiple SVG containers simultaneously with the same options. Each container tracks its own scroll position independently — useful for animating a grid of illustrations at once. Returns a single instance with unified control.

group.js
import { scrollDrawGroup } from 'svg-scroll-draw/group';

const group = scrollDrawGroup(
  ['#hero-svg', '#logo', '#diagram'],
  { easing: 'ease-out', stagger: 0.1, once: true }
);

// All instances controlled together
group.replay();
group.pause();
group.resume();
group.destroy();

Multi-element

Sequence API

Animate multiple containers one after another — each starts only after the previous reaches 100%. Perfect for step-by-step diagram or flowchart reveals.

sequence.js
import { scrollDrawSequence } from 'svg-scroll-draw/group';

const seq = scrollDrawSequence(
  ['#step-1', '#step-2', '#step-3'],
  {
    easing:     'spring',
    onComplete: () => console.log('all steps complete'),
  }
);

seq.replay();   // restarts from step 1
seq.destroy();  // tears down all instances

Hooks

useScrollDrawProgress

A React hook that returns a reactive scroll progress value (0–1) for any element. Uses the same trigger/speed/easing semantics as scrollDraw() — use it to drive any animation alongside or independent of an SVG draw.

ParallaxSection.tsx
import { useRef } from 'react';
import { useScrollDrawProgress } from 'svg-scroll-draw/react';

export function ParallaxSection() {
  const ref      = useRef<HTMLDivElement>(null);
  const progress = useScrollDrawProgress(ref, {
    speed:   1.2,
    easing:  'ease-out',
    trigger: { start: 'top 80%', end: 'center 20%' },
  });

  return (
    <div ref={ref}>
      <div
        style={{
          transform: `translateY(${(1 - progress) * 40}px)`,
          opacity:   progress,
        }}
      >
        <h2>Fades and slides in as you scroll</h2>
      </div>
    </div>
  );
}

Options

speednumberdefault: 1

Same speed multiplier as scrollDraw().

easingEasingName | (t: number) => numberdefault: 'linear'

Same easing curves as scrollDraw().

triggerTriggerConfig

Same trigger syntax. Default: start 'top bottom', end 'bottom top'.

axis'x' | 'y'default: 'y'

Scroll axis.

scrollContainerstring | Element

Custom scroll container.

oncebooleandefault: false

Lock at max progress once reached — never decreases on scroll back.

v1.0.0

createSpring

Returns a custom spring easing function. The built-in 'spring' easing is hardcoded — createSpring lets you tune it.

tensionnumberdefault: 2.5

Oscillation frequency. Higher = more bouncy, faster oscillation.

frictionnumberdefault: 2.2

Damping strength. Higher = less bouncy, settles faster.

spring.js
import { scrollDraw, createSpring } from 'svg-scroll-draw';

// Gentle bounce — close to the built-in 'spring'
scrollDraw('#svg', { easing: createSpring() });

// Tight, snappy spring
scrollDraw('#svg', { easing: createSpring({ tension: 4, friction: 3 }) });

// Slow, wobbly spring
scrollDraw('#svg', { easing: createSpring({ tension: 1.5, friction: 1.2 }) });
Note: createSpring() with no arguments produces the same curve as easing: 'spring'. Use it when you need to parameterize the bounce.

v1.0.0

scrollDrawTimeline

Animate multiple path groups with independent start/end windows within a single scroll range. Unlike stagger (which offsets by time), each track defines its own from/to slice of the 0–1 progress range — and they can overlap freely.

timeline.js
import { scrollDrawTimeline } from 'svg-scroll-draw/timeline';

const instance = scrollDrawTimeline('#diagram', {
  trigger: { start: 'top 80%', end: 'bottom 20%' },
  tracks: [
    // Axes draw first
    { selector: '.axis',    from: 0,    to: 0.3,  easing: 'ease-out' },
    // Bars stagger, each with its own window
    { selector: '.bar-q1',  from: 0.1,  to: 0.45, easing: 'ease-out' },
    { selector: '.bar-q2',  from: 0.28, to: 0.58, easing: 'ease-out' },
    { selector: '.bar-q3',  from: 0.45, to: 0.75, easing: 'ease-out' },
    { selector: '.bar-q4',  from: 0.6,  to: 0.88, easing: 'ease-out' },
    // Trend line traces last
    { selector: '.trend',   from: 0.75, to: 1.0,  easing: 'spring'   },
  ],
  onComplete: () => console.log('all tracks done'),
});

instance.seek(0.5);   // jump to 50% of total range
instance.destroy();

Track options

selectorstring

CSS selector for SVG elements to animate on this track — scoped to the container.

fromnumber

Progress value (0–1) within the overall range where this track starts drawing.

tonumber

Progress value (0–1) within the overall range where this track finishes drawing.

easingEasingName | functiondefault: 'linear'

Easing applied to this track's local progress independently of other tracks.

fadebooleandefault: false

Fade opacity in sync with this track's draw progress.

Timeline-level options

triggerTriggerConfig

Same trigger syntax as scrollDraw().

speednumberdefault: 1

Overall speed multiplier applied to the full range.

oncebooleandefault: false

Lock at max progress once reached.

axis'x' | 'y'default: 'y'

Scroll axis.

onComplete() => void

Fires when the overall progress reaches 1.

v1.0.0

CSS Custom Property

Every scrollDraw() instance automatically sets --scroll-draw-progress on the container element on every animation frame. Use it to drive CSS animations without any JS callbacks.

custom-property.css
/* Drive any CSS property directly from scroll progress */
.hero-text {
  opacity: var(--scroll-draw-progress);
  transform: translateY(calc((1 - var(--scroll-draw-progress)) * 24px));
}

.highlight {
  background-size: calc(var(--scroll-draw-progress) * 100%) 100%;
}

.counter {
  /* Combine with @property for smooth transitions */
  color: oklch(from var(--scroll-draw-progress) 60% 0.2 250);
}
custom-property.js
import { scrollDraw } from 'svg-scroll-draw';

// No onProgress callback needed —
// --scroll-draw-progress is set automatically
scrollDraw('#hero-svg', { easing: 'ease-out', once: true });

// The CSS does the rest:
Note: The value is the progress of the first path (path index 0) in normal mode, and the overall alpha in clip mode. Use onProgress if you need per-path values.

TypeScript

Types Reference

All types are exported from the root package — import them alongside your runtime imports.

types.ts
import type {
  ScrollDrawOptions,
  ScrollDrawInstance,
  EasingName,
  TriggerConfig,
} from 'svg-scroll-draw';

type EasingName = 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'spring';

interface TriggerConfig {
  start?: string;
  end?:   string;
}

interface ScrollDrawInstance {
  destroy:     () => void;
  replay:      () => void;
  pause:       () => void;
  resume:      () => void;
  seek:        (progress: number) => void;
  getProgress: () => number;
}

interface ScrollDrawOptions {
  selector?:        string;
  speed?:           number;
  fade?:            boolean;
  easing?:          EasingName | ((t: number) => number);
  trigger?:         TriggerConfig;
  stagger?:         number;
  direction?:       'forward' | 'reverse';
  once?:            boolean;
  debug?:           boolean;
  axis?:            'x' | 'y';
  scrollContainer?: string | Element;
  autoReverse?:     boolean;
  delay?:           number;
  strokeColor?:     string | [string, string];
  strokeWidth?:     number | [number, number];
  fillOpacity?:     number | [number, number];
  clip?:            boolean | 'left' | 'right' | 'top' | 'bottom' | 'center';
  waypoints?:       Record<number, () => void>;
  velocityScale?:   boolean | number;
  threshold?:       number;
  rootMargin?:      string;
  repeat?:          number | 'infinite';
  repeatDelay?:     number;
  morphTo?:         string;
  onProgress?:      (alpha: number) => void;
  onStart?:         () => void;
  onComplete?:      () => void;
}