Animate SVG on Scroll — svg-scroll-draw API Reference
Getting Started
Installation
Install via your package manager of choice, or drop in a CDN script tag — no build step required. Coming from GSAP DrawSVG? See the migration guide →
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
preset'sketch' | 'reveal' | 'typewriter' | 'cinematic' | 'spring'Apply a named preset as the base configuration. User-supplied options always override the preset. See the Presets section for the full option sets.
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' | 'bounce' | 'elastic' | (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
nativebooleandefault: trueRun the draw as a native CSS scroll-driven animation when the browser supports it and the config is eligible (default trigger, named easing, optional fade, no callbacks or JS-only features). Falls back to the JS engine automatically when not eligible. Set false to always use the JS engine. The full instance API works on both paths.
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
All v2 APIs are available as class-based Refobjects that integrate with Angular's component lifecycle. No peer dependency on @angular/core is required in the library.
ScrollDrawRef — SVG path drawing (v1)
import { Component, ViewChild, ElementRef, AfterViewInit, OnDestroy } from '@angular/core';
import { ScrollDrawRef } from 'svg-scroll-draw/angular';
@Component({
selector: 'app-hero',
template: `<div #container><svg>...</svg></div>`,
})
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(); }
}ScrollAnimateRef — animate any CSS property (v2)
import { ScrollAnimateRef } from 'svg-scroll-draw/angular';
@Component({
selector: 'app-card',
template: `<div #el>...</div>`,
})
export class CardComponent implements AfterViewInit, OnDestroy {
@ViewChild('el') elRef!: ElementRef<HTMLElement>;
private animate = new ScrollAnimateRef();
ngAfterViewInit() {
this.animate.init(this.elRef.nativeElement, {
props: {
opacity: [0, 1],
transform: ['translateY(40px)', 'translateY(0)'],
},
easing: 'ease-out',
once: true,
});
}
ngOnDestroy() { this.animate.destroy(); }
}ScrollCounterRef — animated number (v2)
import { ScrollCounterRef } from 'svg-scroll-draw/angular';
@Component({
selector: 'app-stats',
template: `<span #counter></span>`,
})
export class StatsComponent implements AfterViewInit, OnDestroy {
@ViewChild('counter') counterRef!: ElementRef<HTMLElement>;
private counter = new ScrollCounterRef();
ngAfterViewInit() {
this.counter.init(this.counterRef.nativeElement, {
to: 1_250_000,
format: n => '$' + Math.round(n).toLocaleString(),
once: true,
});
}
ngOnDestroy() { this.counter.destroy(); }
}ScrollTextRef — split text animation (v2)
import { ScrollTextRef } from 'svg-scroll-draw/angular';
@Component({
selector: 'app-headline',
template: `<h2 #headline>Animate on scroll.</h2>`,
})
export class HeadlineComponent implements AfterViewInit, OnDestroy {
@ViewChild('headline') headlineRef!: ElementRef<HTMLElement>;
private text = new ScrollTextRef();
ngAfterViewInit() {
this.text.init(this.headlineRef.nativeElement, {
split: 'words',
stagger: 0.05,
from: { opacity: 0, y: 24 },
once: true,
});
}
ngOnDestroy() { this.text.destroy(); }
}replay(), pause(), resume(), seek(p), getProgress(), destroy(). ScrollVideoRef follows the same pattern for <video> elements.Frameworks
Nuxt
svg-scroll-draw/nuxt re-exports all v1 and v2 Vue composables and components, plus a plugin factory that globally registers all of them at once.
v2 composables — per-component import (recommended)
<script setup>
import { useScrollAnimate, useScrollText, useScrollCounter } from 'svg-scroll-draw/nuxt';
// Animate any CSS property
const card = useScrollAnimate({
props: { opacity: [0, 1], transform: ['translateY(40px)', 'translateY(0)'] },
easing: 'ease-out',
once: true,
});
// Split text and stagger animate
const headline = useScrollText({ split: 'words', stagger: 0.05, once: true });
// Animated counter
const revenue = useScrollCounter({
to: 1_250_000,
format: n => '$' + Math.round(n).toLocaleString(),
once: true,
});
</script>
<template>
<div :ref="card">...</div>
<h2 :ref="headline">Animate on scroll.</h2>
<span :ref="revenue" />
</template>v2 components
<script setup>
import { ScrollAnimate, ScrollText, ScrollCounter } from 'svg-scroll-draw/nuxt';
</script>
<template>
<ScrollAnimate :options="{ props: { opacity: [0,1] }, easing: 'ease-out', once: true }">
<MyCard />
</ScrollAnimate>
<ScrollText :options="{ split: 'words', stagger: 0.05 }" tag="h2">
Animate on scroll.
</ScrollText>
<ScrollCounter :to="1250000" :format="n => '$' + Math.round(n).toLocaleString()" :once="true" />
</template>Global registration via Nuxt plugin
import { createScrollDrawPlugin } from 'svg-scroll-draw/nuxt';
export default defineNuxtPlugin((nuxtApp) => {
// Registers <ScrollDraw>, <ScrollAnimate>, <ScrollCounter>,
// <ScrollVideo>, <ScrollText> globally — no per-component imports.
nuxtApp.vueApp.use(createScrollDrawPlugin());
});Frameworks
Astro
Data-attribute APIs for all v2 animations — no framework runtime needed server-side. Options are passed as inline JSON; a single client script scans and initialises everything.
initScrollDraw — SVG path drawing (v1)
<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>initScrollAnimate — animate any CSS property (v2)
<div
data-scroll-animate
data-scroll-animate-options='{
"props": { "opacity": [0,1], "transform": ["translateY(40px)","translateY(0)"] },
"easing": "ease-out",
"once": true
}'
>
Fades and slides in on scroll.
</div>
<script>
import { initScrollAnimate } from 'svg-scroll-draw/astro';
initScrollAnimate();
</script>initScrollText — split text animation (v2)
<h2
data-scroll-text
data-scroll-text-options='{"split":"words","stagger":0.05,"once":true}'
>
Animate on scroll.
</h2>
<script>
import { initScrollText } from 'svg-scroll-draw/astro';
initScrollText();
</script>initAll — run every init in one call
<!-- Mix any combination of data-scroll-* attributes on the page -->
<div data-scroll-draw data-scroll-draw-options='{"easing":"ease-out"}'><svg>...</svg></div>
<div data-scroll-animate data-scroll-animate-options='{"props":{"opacity":[0,1]},"once":true}'>...</div>
<h2 data-scroll-text data-scroll-text-options='{"split":"words","stagger":0.05}'>..</h2>
<span data-scroll-counter data-scroll-counter-options='{"to":50000,"once":true}'></span>
<script>
import { initAll } from 'svg-scroll-draw/astro';
// Initialises scrollDraw + scrollAnimate + scrollText + scrollCounter in one call
const { draw, animate, text, counter } = initAll();
</script>initAll(document.querySelector('main')) — 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.1.0
Native CSS rendering
For the common case, svg-scroll-drawhands the animation to the browser's native animation-timeline: view(). The draw runs on the compositor with zero per-frame JavaScript and no scroll or resize listeners. It falls back to the JS engine automatically when the browser lacks support, or when the config uses a feature CSS can't express declaratively.
Eligible for native CSS (compositor path)
Default trigger, a named easing, optional fade, forward or reverse direction.
Falls back to JS engine automatically
onProgress / onComplete / waypoints, stagger, morphTo, velocityScale, autoReverse, once, repeat, custom trigger, custom scroll container, speed ≠ 1, spring easing, animated color/width/fill.
// Uses animation-timeline: view() on supporting browsers
scrollDraw('#svg', { easing: 'ease-out', fade: true });
// Force JS engine regardless
scrollDraw('#svg', { native: false, easing: 'spring' });pause, resume, seek, replay, destroy — works on both paths.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.2.0
createBounce
Returns a bounce-out easing. The animation rises to 1 via ease-out, then makes bounces dips below 1 that settle — like a ball landing. Output stays within [0, 1].
bouncesnumberdefault: 3Number of bounces after the initial approach. Higher = more dips before settling.
decaynumberdefault: 0.5Amplitude reduction per bounce (0–1). Lower = faster decay, shallower bounces.
import { scrollDraw, createBounce } from 'svg-scroll-draw';
// Named string — default params (3 bounces, decay 0.5)
scrollDraw('#svg', { easing: 'bounce' });
// Lots of quick bounces
scrollDraw('#svg', { easing: createBounce({ bounces: 5, decay: 0.35 }) });
// Two slow, deep bounces
scrollDraw('#svg', { easing: createBounce({ bounces: 2, decay: 0.7 }) });v1.2.0
createElastic
Returns an elastic-out easing. The animation overshoots past 1 and oscillates back before settling — like a rubber band snapping into place. Can produce values slightly outside [0, 1]; for SVG paths this appears as a brief over-draw.
amplitudenumberdefault: 1Overshoot magnitude (≥1). Default overshoots to ~1.25; try 1.5 for a dramatic snap.
periodnumberdefault: 0.4Oscillation period in scroll-time. Smaller = faster oscillations; larger = slow wobble.
import { scrollDraw, createElastic } from 'svg-scroll-draw';
// Named string — default params (amplitude 1, period 0.4)
scrollDraw('#svg', { easing: 'elastic' });
// Big snap, fast oscillation
scrollDraw('#svg', { easing: createElastic({ amplitude: 1.5, period: 0.3 }) });
// Subtle, slow wobble
scrollDraw('#svg', { easing: createElastic({ amplitude: 1.1, period: 0.6 }) });bounce and elastic with live parameter sliders.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.
repeatnumber | 'infinite'default: 0Replay N times after completing (with once: true). After completion + repeatDelay ms, paths reset and animate again on next scroll-into-view.
repeatDelaynumberdefault: 0Milliseconds to wait before each repeat or loop iteration.
loopboolean | numberdefault: falseAfter the scroll-driven animation completes, automatically replay as a time-driven loop — no further scroll needed. true = loop forever, number = loop N additional times. Each iteration plays over loopDuration ms.
loopDurationnumberdefault: 1500Duration of each time-driven loop iteration in milliseconds.
debugbooleandefault: falseInject a fixed HUD panel into document.bodyshowing each track's scroll window as a coloured progress bar with live fill and global progress. Removed on destroy(). Useful for tuning from/to values.
labelstringLabel shown in the debug panel header. Defaults to the target selector.
v1.6.0
Presets
Named option bags for common scroll-draw patterns. Pass preset as a shorthand — user options always override preset values.
import { scrollDraw } from 'svg-scroll-draw';
// One-liner for common patterns
scrollDraw('#logo', { preset: 'reveal' });
scrollDraw('#diagram', { preset: 'sketch' });
scrollDraw('#text', { preset: 'typewriter' });
scrollDraw('#hero', { preset: 'cinematic' });
scrollDraw('#icon', { preset: 'spring' });
// Override any preset value
scrollDraw('#logo', { preset: 'reveal', easing: 'spring' });
// Inspect presets directly
import { PRESETS } from 'svg-scroll-draw';
console.log(PRESETS.reveal);
// { easing: 'ease-out', fade: true, speed: 1.2, once: true }Available presets
'sketch'presetStaggered ease-in draw — paths trace in one by one. Pencil-drawing feel. Sets: easing: 'ease-in', stagger: 0.1, speed: 0.9.
'reveal'presetClean viewport reveal — fades and draws in once. Best for logos and hero graphics. Sets: easing: 'ease-out', fade: true, speed: 1.2, once: true.
'typewriter'presetFast mechanical draw — paths appear quickly one after another. Sets: easing: 'linear', stagger: 0.05, speed: 1.5.
'cinematic'presetSlow dramatic entrance — long ease-in-out with a gentle fade. Sets: easing: 'ease-in-out', fade: true, speed: 0.75.
'spring'presetBouncy physics feel — spring easing gives a natural overshoot-and-settle. Sets: easing: 'spring', speed: 1.1.
v1.6.0
CLI — npx svg-scroll-draw init
An interactive scaffolder that generates a starter file for your framework — no manual copy-paste from the docs needed.
npx svg-scroll-draw initThe CLI asks for your framework, preset, easing, and SVG selector, then writes a ready-to-use file:
Output by framework
reactWrites ScrollDraw.tsx — a client component with <ScrollDraw> and a sample SVG.
vueWrites ScrollDraw.vue — a SFC with useScrollDraw composable wired up.
svelteWrites ScrollDraw.svelte — uses the use:scrollDraw action.
solidWrites ScrollDraw.tsx — uses createScrollDraw.
vanillaWrites scroll-draw.js — a plain scrollDraw() call.
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.v2.6.0
Vue 3 — v2 composables & components
All v2 APIs now have first-class Vue 3 support. Each API ships as a composable (returns a ref to bind to any element) and a convenience component wrapper. Imports from svg-scroll-draw/vue.
useScrollAnimate + <ScrollAnimate>
<script setup>
import { useScrollAnimate } from 'svg-scroll-draw/vue';
const el = useScrollAnimate({
props: { opacity: [0, 1], transform: ['translateY(40px)', 'translateY(0)'] },
easing: 'ease-out',
once: true,
});
</script>
<template>
<div :ref="el">Animates on scroll.</div>
</template>
<!-- Or use the component wrapper: -->
<ScrollAnimate :options="{ props: { opacity: [0, 1] }, easing: 'ease-out', once: true }">
<div>Animates on scroll.</div>
</ScrollAnimate>useScrollCounter + <ScrollCounter>
<script setup>
import { useScrollCounter } from 'svg-scroll-draw/vue';
const el = useScrollCounter({
to: 1_250_000,
format: n => '$' + Math.round(n).toLocaleString(),
easing: 'ease-out',
once: true,
});
</script>
<template>
<span :ref="el" />
</template>
<!-- Or use the component: -->
<ScrollCounter
:to="1250000"
:format="n => '$' + Math.round(n).toLocaleString()"
easing="ease-out"
:once="true"
/>useScrollVideo + <ScrollVideo>
<script setup>
import { useScrollVideo } from 'svg-scroll-draw/vue';
const vid = useScrollVideo({
trigger: { start: 'top top', end: 'bottom top' },
});
</script>
<template>
<video :ref="vid" src="/hero.mp4" muted playsinline preload="auto" />
</template>
<!-- Or use the component: -->
<ScrollVideo
src="/hero.mp4"
:options="{ trigger: { start: 'top top', end: 'bottom top' } }"
/>useScrollText + <ScrollText>
<script setup>
import { useScrollText } from 'svg-scroll-draw/vue';
const el = useScrollText({
split: 'words',
stagger: 0.05,
from: { opacity: 0, y: 24 },
once: true,
});
</script>
<template>
<h2 :ref="el">Animate on scroll.</h2>
</template>
<!-- Or use the component (tag defaults to "p"): -->
<ScrollText :options="{ split: 'words', stagger: 0.05 }" tag="h2">
Animate on scroll.
</ScrollText>v2.6.0
Svelte — v2 actions & helpers
All v2 APIs are available as Svelte use: actions. Each has a matching create* helper that exposes the live instance for imperative control. Imports from svg-scroll-draw/svelte.
scrollAnimate action
<script>
import { scrollAnimate } from 'svg-scroll-draw/svelte';
const opts = {
props: { opacity: [0, 1], transform: ['translateY(40px)', 'translateY(0)'] },
easing: 'ease-out',
once: true,
};
</script>
<div use:scrollAnimate={opts}>Animates on scroll.</div>scrollCounterAction action
<script>
import { scrollCounterAction } from 'svg-scroll-draw/svelte';
</script>
<span use:scrollCounterAction={{ to: 1250000, format: n => '$' + Math.round(n).toLocaleString(), once: true }} />scrollTextAction action
<script>
import { scrollTextAction } from 'svg-scroll-draw/svelte';
</script>
<h2 use:scrollTextAction={{ split: 'words', stagger: 0.05, once: true }}>
Animate on scroll.
</h2>createScrollAnimate — imperative control
<script>
import { createScrollAnimate } from 'svg-scroll-draw/svelte';
const { action, getInstance } = createScrollAnimate({
props: { opacity: [0, 1] },
easing: 'ease-out',
once: true,
});
</script>
<div use:action>...</div>
<button on:click={() => getInstance()?.replay()}>Replay</button>create* pattern is available for all v2 actions: createScrollCounter, createScrollVideo, createScrollText.v2.6.0
Solid.js — v2 hooks
All v2 APIs are available as SolidJS hooks. Each use* hook returns a ref setter. The matching create* variant returns { ref, getInstance } for imperative control. Imports from svg-scroll-draw/solid.
useScrollAnimate
import { useScrollAnimate } from 'svg-scroll-draw/solid';
function Hero() {
const ref = useScrollAnimate({
props: { opacity: [0, 1], transform: ['translateY(40px)', 'translateY(0)'] },
easing: 'ease-out',
once: true,
});
return <div ref={ref}>Animates on scroll.</div>;
}useScrollCounter
import { useScrollCounter } from 'svg-scroll-draw/solid';
function Revenue() {
const ref = useScrollCounter({
to: 1_250_000,
format: n => '$' + Math.round(n).toLocaleString(),
easing: 'ease-out',
once: true,
});
return <span ref={ref} />;
}useScrollText
import { useScrollText } from 'svg-scroll-draw/solid';
function Headline() {
const ref = useScrollText({
split: 'words',
stagger: 0.05,
from: { opacity: 0, y: 24 },
once: true,
});
return <h2 ref={ref}>Animate on scroll.</h2>;
}createScrollAnimate — imperative control
import { createScrollAnimate } from 'svg-scroll-draw/solid';
function Hero() {
const { ref, getInstance } = createScrollAnimate({
props: { opacity: [0, 1] },
easing: 'ease-out',
once: true,
});
return (
<>
<div ref={ref}>...</div>
<button onClick={() => getInstance()?.replay()}>Replay</button>
</>
);
}create* pattern is available for all v2 hooks: createScrollCounter, createScrollVideo, createScrollText.v2.9.0
scrollProgress
Expose scroll progress as CSS custom properties on a target element. Drive opacity, transforms, colors, gradients — anything CSS can express — directly from calc() expressions with zero per-frame JS beyond the variable write. Two properties are set: raw (linear) and eased.
import { scrollProgress } from 'svg-scroll-draw/progress';
// Set --scroll-progress (raw) and --scroll-progress-eased on the element
scrollProgress('#hero', { easing: 'ease-in-out' });
// Custom variable names
scrollProgress('#section', {
variable: '--my-progress',
easedVariable: '--my-progress-eased',
trigger: { start: 'top 80%', end: 'bottom 20%' },
});
// Use in CSS
// #hero {
// opacity: calc(var(--scroll-progress-eased));
// transform: translateY(
// calc((1 - var(--scroll-progress-eased)) * 40px)
// );
// }| Option | Type | Default | Description |
|---|---|---|---|
| variable | string | "--scroll-progress" | Raw (linear) 0–1 progress property. |
| easedVariable | string | null | "--scroll-progress-eased" | Eased 0–1 progress property. Set to null to skip. |
| trigger | TriggerConfig | top bottom / bottom top | Scroll window for 0→1 mapping. |
| easing | EasingName | fn | "linear" | Easing applied to the eased variable. |
| speed | number | 1 | Animation speed scale. |
| axis | "x" | "y" | "y" | Scroll axis. |
| scrollContainer | string | Element | — | Custom scroll container. |
| onProgress | (raw, eased) => void | — | Callback fired every frame with both values. |
v2.9.0
scrollHorizontal
Drive horizontal translateXfrom vertical scroll — the Apple / Stripe “scroll sideways” pattern. You handle the sticky CSS;scrollHorizontal drives the transform.
import { scrollHorizontal } from 'svg-scroll-draw/horizontal';
// Minimal CSS:
// .outer { height: 400vh; }
// .sticky { position: sticky; top: 0; height: 100vh; overflow: hidden; }
// .track { display: flex; width: max-content; }
const inst = scrollHorizontal('.track', {
distance: window.innerWidth * 3, // total horizontal travel
easing: 'linear', // scrub feel
trigger: {
start: 'top top',
end: 'bottom bottom',
},
onProgress: (p) => dots.setActive(Math.round(p * 3)),
});
// React
useEffect(() => {
const inst = scrollHorizontal(trackRef.current, {
distance: trackRef.current.scrollWidth - window.innerWidth,
});
return () => inst.destroy();
}, []);
inst.refresh(); // recalculate after layout change
inst.destroy(); // cleanupv2.8.0
scrollReveal
Reveal elements as they scroll into view. One-line replacement for AOS and ScrollReveal.js — no data attributes, fully typed, 7 named presets, stagger, and custom easing.
import { scrollReveal } from 'svg-scroll-draw/reveal';
// Default: fade up
scrollReveal('.card');
// Named preset
scrollReveal('.feature', { preset: 'scale' });
// Custom from state
scrollReveal('.item', {
from: { opacity: 0, y: 40, scale: 0.95 },
stagger: 0.1,
easing: 'ease-out',
once: true,
});
// Presets: 'fadeUp' | 'fadeDown' | 'fadeLeft' | 'fadeRight' | 'scale' | 'flip' | 'flipX'
// Cleanup
const instance = scrollReveal('.card', { preset: 'fadeUp' });
instance.destroy();| Option | Type | Default | Description |
|---|---|---|---|
| preset | ScrollRevealPreset | "fadeUp" | Named start state. fadeUp | fadeDown | fadeLeft | fadeRight | scale | flip | flipX |
| from | ScrollRevealFrom | — | Custom start state: { opacity, x, y, scale, rotate, rotateX, rotateY }. Merges with preset. |
| stagger | number | 0.08 | Viewport-% offset per element — creates cascade for lists. |
| easing | EasingName | fn | "ease-out" | Animation easing. |
| once | boolean | true | Freeze at max progress — don't reverse on scroll back. |
| trigger | TriggerConfig | auto | Override trigger window. Default computed per element. |
| onEnter | () => void | — | Fires when the first element enters its trigger zone. |
| onLeave | () => void | — | Fires when the last element leaves its trigger zone. |
v2.8.0
velocityScale
Scale animation speed by scroll velocity — the faster the user scrolls, the faster the animation progresses. Available on scrollAnimate (and already on scrollDraw). Pass true for default sensitivity or a number to tune it.
import { scrollAnimate } from 'svg-scroll-draw';
// Default sensitivity (1) — moderate speed-up on fast scroll
scrollAnimate('#hero', {
props: { opacity: [0, 1], transform: ['translateY(32px)', 'translateY(0)'] },
velocityScale: true,
});
// High sensitivity (3) — very responsive to scroll speed
scrollAnimate('#kinetic-bg', {
props: { transform: ['translateY(0px)', 'translateY(-200px)'] },
velocityScale: 3,
easing: 'linear',
});
// velocityScale is available on scrollDraw too:
scrollDraw('#logo', {
easing: 'ease-out',
velocityScale: 1.5,
});velocityScale forces the JS engine — the native CSS fast path is skipped because velocity requires per-frame measurement.v2.7.0
scrollPin
Pin any element at a viewport position while the page scrolls past it. Wrapper-based — inserts a spacer to prevent layout shift. Full lifecycle callbacks mirror GSAP ScrollTrigger's pin: true.
import { scrollPin } from 'svg-scroll-draw/pin';
// Pin at top of viewport for one viewport-height of scroll
const pin = scrollPin('#panel', {
pinDistance: window.innerHeight,
onEnter: () => panel.classList.add('active'),
onLeave: () => panel.classList.remove('active'),
onEnterBack: () => panel.classList.add('active'),
onLeaveBack: () => panel.classList.remove('active'),
onProgress: (p) => console.log('progress', p), // 0–1 through pin zone
});
// Apple-style: pin image, scroll text past it
scrollPin('#product-image', {
top: 80, // pin 80px from viewport top (below a fixed nav)
pinDistance: 800, // hold for 800px of scroll
});
// Recalculate after layout change (accordion, dynamic content)
pin.refresh();
// Remove on unmount / route change
pin.destroy();| Option | Type | Default | Description |
|---|---|---|---|
| pinDistance | number | window.innerHeight | Pixels of scroll to stay pinned. |
| top | number | 0 | Viewport Y (px) to pin at. 0 = top of viewport. |
| scrollContainer | string|Element | — | Custom scroll container. Default: window. |
| onEnter | () => void | — | Fires when scroll enters the pin zone (scrolling down). |
| onLeave | () => void | — | Fires when scroll exits the pin zone at the end. |
| onEnterBack | () => void | — | Fires when scroll re-enters the pin zone (scrolling up). |
| onLeaveBack | () => void | — | Fires when scroll exits the pin zone at the start. |
| onProgress | (p: number) => void | — | Progress 0–1 through the pin zone, every frame. |
destroy() restores the original DOM and removes all listeners. refresh() recalculates pin dimensions — call it after layout changes.v2.7.0
scrollSnap
JS-powered section snapping with custom easing and a configurable threshold. Works on vertical and horizontal axes with any scroll container. Unlike CSS scroll-snap-type, you get callbacks, programmatic control, and custom easing curves.
import { scrollSnap } from 'svg-scroll-draw/snap';
// Basic vertical snap between fullscreen sections
const snap = scrollSnap('.section', {
duration: 600,
easing: 'ease-in-out',
threshold: 0.3, // snap forward if user scrolled >30% of a section
onSnap: (index) => console.log('snapped to section', index),
});
// Horizontal snap (e.g. feature carousel)
scrollSnap('.card', {
direction: 'horizontal',
duration: 400,
easing: 'ease-out',
onSnap: (i) => setActiveCard(i),
});
// Programmatic control
snap.snapTo(2); // scroll to section index 2
snap.getCurrentIndex(); // → current snapped index
// Cleanup
snap.destroy();| Option | Type | Default | Description |
|---|---|---|---|
| direction | "vertical" | "horizontal" | "vertical" | Scroll axis. |
| duration | number | 600 | Snap animation duration in ms. |
| easing | EasingName | fn | "ease-in-out" | Easing for the snap scroll animation. |
| threshold | number | 0.3 | Fraction of a section size the user must scroll past to snap forward (0–1). |
| scrollContainer | string | Element | — | Custom scroll container. Default: window. |
| onSnap | (index: number) => void | — | Fires after each snap with the target section index. |
v2.7.0
Scroll Callbacks
onEnter, onLeave, onEnterBack, and onLeaveBack are available on both scrollAnimate and scrollDraw. They fire exactly once per crossing — same semantics as GSAP ScrollTrigger.
import { scrollAnimate } from 'svg-scroll-draw';
// Activate/deactivate a section as it enters and leaves view
scrollAnimate('#feature-section', {
props: { opacity: [0.4, 1] },
trigger: { start: 'top center', end: 'bottom center' },
onEnter: () => nav.setActive('feature'), // scrolling down, entering
onLeave: () => nav.clearActive('feature'), // scrolling down, leaving
onEnterBack: () => nav.setActive('feature'), // scrolling up, re-entering
onLeaveBack: () => nav.clearActive('feature'), // scrolling up, leaving from top
});
// Lazy-load an image when it first enters view
scrollAnimate('#hero-img', {
props: { opacity: [0, 1] },
once: true,
onEnter: () => {
document.querySelector('#hero-img')
.setAttribute('src', '/hero.webp');
},
});animation-timeline fast path is skipped because callbacks require per-frame JavaScript.v2.7.0
Lenis Adapter
Lenis v2+ patches window.scrollY natively — no adapter needed, svg-scroll-draw works out of the box. For Lenis v1 (which uses a virtual scroll value), use createLenisAdapter to keep all engines in sync.
import Lenis from '@studio-freight/lenis'; // Lenis v1
import { createLenisAdapter } from 'svg-scroll-draw/lenis';
import { scrollAnimate } from 'svg-scroll-draw';
// 1. Create Lenis
const lenis = new Lenis({ duration: 1.2, easing: t => Math.min(1, 1.001 - Math.pow(2, -10 * t)) });
// 2. Patch window.scrollY so all svg-scroll-draw engines read the virtual scroll
const adapter = createLenisAdapter(lenis);
// 3. Use svg-scroll-draw as normal
scrollAnimate('#hero', {
props: { opacity: [0, 1], transform: ['translateY(40px)', 'translateY(0)'] },
once: true,
});
// 4. Drive Lenis with rAF
function raf(time) {
lenis.raf(time);
requestAnimationFrame(raf);
}
requestAnimationFrame(raf);
// 5. Cleanup
adapter.destroy();
lenis.destroy();
// ── Lenis v2 (no adapter needed) ──────────────────────────────────
import Lenis from 'lenis'; // v2 patches window.scrollY natively
const lenis = new Lenis();
lenis.on('scroll', ScrollTrigger.update); // optional if mixing with other libs
// svg-scroll-draw just works
scrollAnimate('#hero', { props: { opacity: [0, 1] }, once: true });v2.0.0
scrollAnimate
Animate any CSS property on any DOM or SVG element driven by scroll position. The direct replacement for gsap.to(el, { scrollTrigger }) for the 80% case.
import { scrollAnimate } from 'svg-scroll-draw';
// Fade + slide in
scrollAnimate('#hero-text', {
props: {
opacity: [0, 1],
transform: ['translateY(40px)', 'translateY(0px)'],
},
easing: 'ease-out',
once: true,
});
// Color transition
scrollAnimate('#section', {
props: {
backgroundColor: ['#ffffff', '#0d0d0d'],
color: ['#000000', '#ffffff'],
},
});
// React wrapper
import { ScrollAnimate } from 'svg-scroll-draw/react';
<ScrollAnimate
props={{ opacity: [0, 1], transform: ['translateY(24px)', 'translateY(0)'] }}
easing="ease-out"
once
>
<div>Any content</div>
</ScrollAnimate>| Option | Type | Default | Description |
|---|---|---|---|
| props | Record<string, …> | — | CSS properties to animate. Value can be a single target or [from, to] tuple. Supports numbers, colors (hex/rgb), transform functions, and CSS units. |
| trigger | TriggerConfig | { start: "top bottom", end: "bottom top" } | Same trigger syntax as scrollDraw. |
| easing | EasingName | fn | "ease-out" | Same easing system as scrollDraw. |
| speed | number | 1 | Animation scale factor. |
| once | boolean | false | Freeze at max progress — does not reverse on scroll back. |
| axis | "x" | "y" | "y" | Scroll axis. |
| native | boolean | true | Use CSS animation-timeline: view() fast path when eligible. |
| onProgress | (n) => void | — | Called every frame with alpha 0–1. |
| onComplete | () => void | — | Fires when alpha reaches 1. |
| onEnter | () => void | — | Fires when scroll enters the trigger zone (scrolling forward). Forces JS engine. |
| onLeave | () => void | — | Fires when scroll exits the trigger zone at the end (scrolling forward). |
| onEnterBack | () => void | — | Fires when scroll re-enters the trigger zone from the end (scrolling back). |
| onLeaveBack | () => void | — | Fires when scroll exits the trigger zone at the start (scrolling back). |
v2.0.0
scrollCounter
Animate a number displayed in a DOM element from from to to as it scrolls into view.
import { scrollCounter } from 'svg-scroll-draw';
// Simple counter
scrollCounter('#user-count', { to: 50000 });
// Formatted with locale
scrollCounter('#revenue', {
to: 1_250_000,
format: n => '$' + Math.round(n).toLocaleString(),
easing: 'ease-out',
once: true,
});
// Percentage with decimal
scrollCounter('#conversion', {
from: 0,
to: 94.7,
decimals: 1,
format: n => n.toFixed(1) + '%',
});
// React
import { ScrollCounter } from 'svg-scroll-draw/react';
<ScrollCounter to={50000} format={n => n.toLocaleString()} once />v2.0.0
scrollParallax
Move any element at a different rate than scroll. speed is a multiplier relative to element size — 0.5 = half scroll speed, -0.2 = opposite direction.
import { scrollParallax } from 'svg-scroll-draw';
scrollParallax('#hero-bg-image', { speed: 0.4 });
scrollParallax('#floating-element', { speed: -0.2 }); // moves opposite
scrollParallax('#side-badge', { speed: 0.3, axis: 'x' }); // horizontalv2.1.0
scrollVideo
Tie a <video> element's currentTime to scroll position. The Apple product page pattern — ships free.
import { scrollVideo } from 'svg-scroll-draw/video';
// Full scrub — plays as user scrolls through section
scrollVideo('#hero-video', {
trigger: { start: 'top top', end: 'bottom top' },
});
// Scrub only the first 3 seconds
scrollVideo('#product-reveal', {
from: 0,
to: 3,
trigger: { start: 'top 80%', end: 'top 20%' },
easing: 'ease-in-out',
});
// React
import { ScrollVideo } from 'svg-scroll-draw/react';
<ScrollVideo src="/hero.mp4" trigger={{ start: 'top top', end: 'bottom top' }} />Video encoding tip: Use H.264 for broadest support. Add muted playsinline preload="auto" attributes for smooth scrubbing.
v2.1.0
scrollText
Split any text element into chars, words, or lines and animate each piece on scroll. Free replacement for GSAP SplitText (which requires a $150+/yr Club GreenSock subscription).
import { scrollText } from 'svg-scroll-draw/text';
// Words fade up one by one
scrollText('#headline', {
split: 'words',
stagger: 0.05,
from: { opacity: 0, y: 24 },
easing: 'ease-out',
once: true,
});
// Characters with rotation
scrollText('#tagline', {
split: 'chars',
stagger: 0.025,
from: { opacity: 0, y: 32, rotate: 8 },
once: true,
});
// React
import { ScrollText } from 'svg-scroll-draw/react';
<ScrollText split="words" stagger={0.05} from={{ opacity: 0, y: 24 }} once>
Animate this headline.
</ScrollText>Accessibility: the original text is preserved in aria-label on the container. All split spans have aria-hidden="true". destroy() restores the original HTML.
v2.2.0
DevTools Overlay
Visual debug overlay that shows every active animation's trigger window, current progress, and type — color-coded in a fixed panel. Zero production bytes (tree-shaken away in production builds).
import { devtools } from 'svg-scroll-draw/devtools';
// Enable once — instruments all active instances
devtools.enable();
// Keyboard shortcut: Cmd+Shift+S / Ctrl+Shift+S to toggle
// Or toggle programmatically:
devtools.toggle();
// Highlight a specific element for 2 seconds
devtools.highlight('#my-animated-element');Note: The devtools panel only activates when process.env.NODE_ENV !== 'production'. In production builds, the import resolves to a no-op.
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;
native?: boolean;
}