InstallationQuick StartCoreTriggerVisualCallbacksAdvancedMethodsReactVue 3SvelteSolid.jsAngularNuxtAstroWeb ComponentVanilla JSGroup APISequence APIuseScrollDrawProgressscrollProgressscrollHorizontalscrollRevealvelocityScalescrollPinscrollSnapScroll CallbacksLenis AdapterVue 3 v2Svelte v2Solid v2scrollAnimatescrollCounterscrollParallaxscrollVideoscrollTextDevToolsPresetsCLI initNative CSScreateSpringcreateBouncecreateElasticscrollDrawTimelineCSS Custom PropTypes

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 →

$ 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

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

nativebooleandefault: true

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

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)

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>...</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)

card.component.ts
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)

stats.component.ts
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)

headline.component.ts
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(); }
}
Note: All Ref classes expose the full instance API: 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)

pages/index.vue
<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

pages/index.vue
<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

plugins/svg-scroll-draw.ts
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)

src/pages/index.astro
<div
  data-scroll-draw
  data-scroll-draw-options='{"easing":"ease-out","fade":true,"once":true}'
>
  <svg viewBox="0 0 200 100" fill="none">
    <path d="M10 50 Q100 10 190 50" stroke="black" stroke-width="2" />
  </svg>
</div>

<script>
  import { initScrollDraw } from 'svg-scroll-draw/astro';
  initScrollDraw();
</script>

initScrollAnimate — animate any CSS property (v2)

src/pages/index.astro
<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)

src/pages/index.astro
<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

src/pages/index.astro
<!-- 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>
Note: Pass a root element to any init function — e.g. 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.

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

native.js
// Uses animation-timeline: view() on supporting browsers
scrollDraw('#svg', { easing: 'ease-out', fade: true });

// Force JS engine regardless
scrollDraw('#svg', { native: false, easing: 'spring' });
Note: The full instance API — 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.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.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: 3

Number of bounces after the initial approach. Higher = more dips before settling.

decaynumberdefault: 0.5

Amplitude reduction per bounce (0–1). Lower = faster decay, shallower bounces.

bounce.js
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: 1

Overshoot magnitude (≥1). Default overshoots to ~1.25; try 1.5 for a dramatic snap.

periodnumberdefault: 0.4

Oscillation period in scroll-time. Smaller = faster oscillations; larger = slow wobble.

elastic.js
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 }) });
Note: Try both in the ⚡ Playground — the easing dropdown includes 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.

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.

repeatnumber | 'infinite'default: 0

Replay N times after completing (with once: true). After completion + repeatDelay ms, paths reset and animate again on next scroll-into-view.

repeatDelaynumberdefault: 0

Milliseconds to wait before each repeat or loop iteration.

loopboolean | numberdefault: false

After 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: 1500

Duration of each time-driven loop iteration in milliseconds.

debugbooleandefault: false

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

labelstring

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

presets.js
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'preset

Staggered ease-in draw — paths trace in one by one. Pencil-drawing feel. Sets: easing: 'ease-in', stagger: 0.1, speed: 0.9.

'reveal'preset

Clean 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'preset

Fast mechanical draw — paths appear quickly one after another. Sets: easing: 'linear', stagger: 0.05, speed: 1.5.

'cinematic'preset

Slow dramatic entrance — long ease-in-out with a gentle fade. Sets: easing: 'ease-in-out', fade: true, speed: 0.75.

'spring'preset

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

terminal
npx svg-scroll-draw init

The CLI asks for your framework, preset, easing, and SVG selector, then writes a ready-to-use file:

Output by framework

react

Writes ScrollDraw.tsx — a client component with <ScrollDraw> and a sample SVG.

vue

Writes ScrollDraw.vue — a SFC with useScrollDraw composable wired up.

svelte

Writes ScrollDraw.svelte — uses the use:scrollDraw action.

solid

Writes ScrollDraw.tsx — uses createScrollDraw.

vanilla

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

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.

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>

Hero.vue
<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>

Stats.vue
<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>

HeroVideo.vue
<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>

Headline.vue
<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

Hero.svelte
<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

Stats.svelte
<script>
  import { scrollCounterAction } from 'svg-scroll-draw/svelte';
</script>

<span use:scrollCounterAction={{ to: 1250000, format: n => '$' + Math.round(n).toLocaleString(), once: true }} />

scrollTextAction action

Headline.svelte
<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

Hero.svelte
<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>
Note: The same 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

Hero.tsx
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

Stats.tsx
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

Headline.tsx
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

Hero.tsx
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>
    </>
  );
}
Note: The same 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.

index.js
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)
//   );
// }
OptionTypeDefaultDescription
variablestring"--scroll-progress"Raw (linear) 0–1 progress property.
easedVariablestring | null"--scroll-progress-eased"Eased 0–1 progress property. Set to null to skip.
triggerTriggerConfigtop bottom / bottom topScroll window for 0→1 mapping.
easingEasingName | fn"linear"Easing applied to the eased variable.
speednumber1Animation speed scale.
axis"x" | "y""y"Scroll axis.
scrollContainerstring | ElementCustom scroll container.
onProgress(raw, eased) => voidCallback 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.

index.js
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();  // cleanup

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

index.js
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();
OptionTypeDefaultDescription
presetScrollRevealPreset"fadeUp"Named start state. fadeUp | fadeDown | fadeLeft | fadeRight | scale | flip | flipX
fromScrollRevealFromCustom start state: { opacity, x, y, scale, rotate, rotateX, rotateY }. Merges with preset.
staggernumber0.08Viewport-% offset per element — creates cascade for lists.
easingEasingName | fn"ease-out"Animation easing.
oncebooleantrueFreeze at max progress — don't reverse on scroll back.
triggerTriggerConfigautoOverride trigger window. Default computed per element.
onEnter() => voidFires when the first element enters its trigger zone.
onLeave() => voidFires 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.

index.js
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,
});
Note: 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.

index.js
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();
OptionTypeDefaultDescription
pinDistancenumberwindow.innerHeightPixels of scroll to stay pinned.
topnumber0Viewport Y (px) to pin at. 0 = top of viewport.
scrollContainerstring|ElementCustom scroll container. Default: window.
onEnter() => voidFires when scroll enters the pin zone (scrolling down).
onLeave() => voidFires when scroll exits the pin zone at the end.
onEnterBack() => voidFires when scroll re-enters the pin zone (scrolling up).
onLeaveBack() => voidFires when scroll exits the pin zone at the start.
onProgress(p: number) => voidProgress 0–1 through the pin zone, every frame.
Note: Instance API: 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.

index.js
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();
OptionTypeDefaultDescription
direction"vertical" | "horizontal""vertical"Scroll axis.
durationnumber600Snap animation duration in ms.
easingEasingName | fn"ease-in-out"Easing for the snap scroll animation.
thresholdnumber0.3Fraction of a section size the user must scroll past to snap forward (0–1).
scrollContainerstring | ElementCustom scroll container. Default: window.
onSnap(index: number) => voidFires 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.

index.js
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');
  },
});
Note: Adding any of these callbacks forces the JS engine — the native CSS 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.

index.js
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.

index.js
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>
OptionTypeDefaultDescription
propsRecord<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.
triggerTriggerConfig{ start: "top bottom", end: "bottom top" }Same trigger syntax as scrollDraw.
easingEasingName | fn"ease-out"Same easing system as scrollDraw.
speednumber1Animation scale factor.
oncebooleanfalseFreeze at max progress — does not reverse on scroll back.
axis"x" | "y""y"Scroll axis.
nativebooleantrueUse CSS animation-timeline: view() fast path when eligible.
onProgress(n) => voidCalled every frame with alpha 0–1.
onComplete() => voidFires when alpha reaches 1.
onEnter() => voidFires when scroll enters the trigger zone (scrolling forward). Forces JS engine.
onLeave() => voidFires when scroll exits the trigger zone at the end (scrolling forward).
onEnterBack() => voidFires when scroll re-enters the trigger zone from the end (scrolling back).
onLeaveBack() => voidFires 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.

index.js
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.

index.js
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' }); // horizontal

v2.1.0

scrollVideo

Tie a <video> element's currentTime to scroll position. The Apple product page pattern — ships free.

index.js
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).

index.js
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).

main.js
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.

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;
  native?:          boolean;
}