Getting Started
Installation
Install via your package manager of choice, or drop in a CDN script tag — no build step required.
CDN
<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.
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 unmountstroke 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: 1Animation 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: 0Delay 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: falseLock 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."
// 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: falseFade 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'.
morphTostringTarget 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.
// 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) => voidCalled 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() => voidFires once when the animation begins — the first frame where progress > 0.
onComplete() => voidFires 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().
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: falseAutomatically 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 | ElementCSS selector or Element reference for a custom scroll container. Defaults to window.
delaynumberdefault: 0Milliseconds to wait before the engine starts observing. Useful for staggering multiple instances on initial page load.
velocityScaleboolean | numberdefault: falseScale animation speed by scroll velocity — faster scrolling draws faster. Pass a number to control sensitivity (default is 1).
repeatnumber | 'infinite'default: 0Repeat the animation N times after completion. Use 'infinite' to loop forever.
repeatDelaynumberdefault: 0Milliseconds to wait between animation repeats.
debugbooleandefault: falseRenders a visual overlay showing the start and end trigger zones. Stripped in production — dev only.
thresholdnumberdefault: 0IntersectionObserver 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()() => voidDisconnects the IntersectionObserver, cancels the rAF loop, and removes all event listeners. Always call this on component unmount.
replay()() => voidResets to initial state and replays from the beginning. Clears the once lock and waypoint history.
pause()() => voidPauses the rAF loop at the current progress. Scroll position is still tracked.
resume()() => voidResumes a paused animation from where it stopped.
seek(progress)(progress: number) => voidJump to a specific progress value (0–1) and pause. Useful for building scrubber controls.
getProgress()() => numberReturns the current draw progress as a number between 0 and 1.
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.
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>
);
}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
<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
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.
<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.
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.
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)
<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
import { createScrollDrawPlugin } from 'svg-scroll-draw/nuxt';
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(createScrollDrawPlugin());
});
// <ScrollDraw> is now available globally — no per-component imports neededFrameworks
Astro
initScrollDraw() auto-initialises all elements with a data-scroll-draw attribute. Options are read from a JSON attribute.
---
// 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>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.
<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
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.
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.
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 instancesHooks
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.
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: 1Same speed multiplier as scrollDraw().
easingEasingName | (t: number) => numberdefault: 'linear'Same easing curves as scrollDraw().
triggerTriggerConfigSame trigger syntax. Default: start 'top bottom', end 'bottom top'.
axis'x' | 'y'default: 'y'Scroll axis.
scrollContainerstring | ElementCustom scroll container.
oncebooleandefault: falseLock 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.5Oscillation frequency. Higher = more bouncy, faster oscillation.
frictionnumberdefault: 2.2Damping strength. Higher = less bouncy, settles faster.
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 }) });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.
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
selectorstringCSS selector for SVG elements to animate on this track — scoped to the container.
fromnumberProgress value (0–1) within the overall range where this track starts drawing.
tonumberProgress 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: falseFade opacity in sync with this track's draw progress.
Timeline-level options
triggerTriggerConfigSame trigger syntax as scrollDraw().
speednumberdefault: 1Overall speed multiplier applied to the full range.
oncebooleandefault: falseLock at max progress once reached.
axis'x' | 'y'default: 'y'Scroll axis.
onComplete() => voidFires 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.
/* 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);
}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:onProgress if you need per-path values.TypeScript
Types Reference
All types are exported from the root package — import them alongside your runtime imports.
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;
}