Real-world examples
Seventeen production-ready patterns — logo reveals, charts, signatures, video scrub, scrollAnimateGroup, feature reveals, Vue 3, Svelte, Solid.js, Timeline API, Group API, Sequence API, and more. Each one is powered by svg-scroll-draw and works in React, Vue 3, Svelte, Solid, and vanilla JS. Scroll down to see them draw live.
01
·onComplete · fill transitionThe Discord logo strokes itself in as an outline, then floods with color on completion — followed by a right-eye blink. Uses onComplete to trigger the fill transition and blink sequence.
// 1. stroke draws on scroll (fill starts transparent)
// 2. onComplete: fill floods in + right eye blinks
<ScrollDraw easing="ease-out" speed={0.95} once
onComplete={handleComplete}>
<svg viewBox="0 0 127.14 96.36">
<path d="M107.7,8.07…"
fill={filled ? '#5865F2' : 'transparent'}
stroke="#5865F2" strokeWidth="2.5"
style={{ transition: 'fill 0.45s ease' }} />
{/* Left eye — floods white on complete */}
<ellipse cx="42.45" cy="53.03" rx="11.42" ry="12.67"
fill={filled ? 'white' : 'transparent'}
style={{ transition: 'fill 0.45s ease' }} />
{/* Right eye — blinks via scaleY */}
<ellipse cx="84.69" cy="53.03" rx="11.42" ry="12.67"
fill={filled ? 'white' : 'transparent'}
style={{ transform: `scaleY(${blinking ? 0.05 : 1})`,
transformBox: 'fill-box', transformOrigin: 'center',
transition: 'fill 0.45s ease, transform 0.1s ease-in-out' }} />
</svg>
</ScrollDraw>02
·selector · strokeColorMonthly revenue line draws itself across the chart axes on scroll — axes animate first, then the data line traces in with a color shift from grey to brand pink.
<ScrollDraw
easing="ease-in-out" speed={0.85} once
selector=".ink"
strokeColor={['#e2e8f0', '#ff90e8']}
>
<svg>
{/* Static: grid, labels, data points, area */}
{/* .ink elements animate: axes + data line */}
<line className="ink" {/* y-axis */} />
<line className="ink" {/* x-axis */} />
<path className="ink" d="M 55 138 C…" {/* data line */} />
</svg>
</ScrollDraw>03
·stagger · ease-outCEO signature draws on scroll with a staggered underline flourish — label, printed name and date are static context. Looks like ink drying on a real document.
<ScrollDraw
easing="ease-out" speed={0.6}
stagger={0.35} once
>
<svg>
{/* Static: "Authorized Signature" label, name, date */}
{/* Animated path 1: cursive signature */}
<path d="M 18 90 C 14 62…" stroke="#111" strokeWidth="2.2" />
{/* Animated path 2: underline flourish (starts after sig) */}
<path d="M 12 112 C 90 122…" stroke="#ff90e8" />
</svg>
</ScrollDraw>04
·selector · staggerBox borders and arrow connectors draw in sequence across a 4-step checkout process. Static fills and emoji keep the diagram readable before animation starts.
<ScrollDraw
easing="ease-out" speed={1.1}
selector=".ink" stagger={0.18} once
>
<svg>
{/* Static: filled boxes + emoji + step labels */}
<rect x={8} fill="#f0f4ff" />{/* Cart */}
<rect x={82} fill="#fff8f0" />{/* Shipping */}
{/* .ink elements animate: borders + arrows */}
<rect className="ink" x={8} stroke="#5865F2" />
<path className="ink" d="M 70 70 L 82 70" {/* arrow */} />
</svg>
</ScrollDraw>05
·selector · strokeColorA delivery route traces itself across a city block map — warehouse to customer. City blocks, street names and the distance badge are static; only the route line animates.
<ScrollDraw
easing="ease-in-out" speed={0.75} once
selector=".ink"
strokeColor={['#fbbf24', '#ff90e8']}
>
<svg>
{/* Static: city blocks, street labels, distance badge */}
{/* Static: start/end markers */}
{/* Animated: route path only */}
<path className="ink"
d="M 44 175 L 44 132 L 90 132…"
stroke="#fbbf24" strokeWidth="4" />
</svg>
</ScrollDraw>06
·selector · ease-outConnection lines between services draw in on scroll — Browser → API Gateway → Auth / Database / Cache. Service boxes and labels are always visible; only the wires animate.
<ScrollDraw
easing="ease-out" speed={1.1} once
selector=".ink"
>
<svg>
{/* Static: labeled service boxes */}
<rect … /><text>🌐 Browser</text>
<rect … /><text>API Gateway</text>
{/* .ink lines draw the connections */}
<line className="ink" {/* Browser → API */} />
<line className="ink" {/* API → Auth */} strokeDasharray="4 3" />
<line className="ink" {/* API → DB */} strokeDasharray="4 3" />
<line className="ink" {/* API → Cache */} strokeDasharray="4 3" />
</svg>
</ScrollDraw>07
·data-scroll-draw · initScrollDraw()Zero-JS server components — add data-scroll-draw to any element and call initScrollDraw() in a client script. Options are passed as a JSON attribute. No framework wrapper needed.
// src/pages/index.astro
---
// No server-side imports needed
---
<div
data-scroll-draw
data-scroll-draw-options='{"easing":"ease-out","fade":true,"once":true}'
>
<svg viewBox="0 0 200 80" fill="none">
<path d="M10 40 Q100 5 190 40"
stroke="white" strokeWidth="2" />
</svg>
</div>
<script>
import { initScrollDraw } from 'svg-scroll-draw/astro';
// Finds all [data-scroll-draw] on the page and
// initialises each one — no React, no Vue needed.
initScrollDraw();
</script>08
·scrollDrawTimeline · independent tracksFour quarterly bars and a trend line — each animated on its own independent scroll window using scrollDrawTimeline. Axes draw first (0→28%), then Q1–Q4 bars stagger across (10→88%), and the trend line traces last (75→100%) once all bars are fully visible.
import { scrollDrawTimeline } from 'svg-scroll-draw/timeline';
// Each track owns its own from/to slice of the scroll range.
// Unlike stagger (time offset), windows can overlap freely.
scrollDrawTimeline('#chart', {
trigger: { start: 'top 88%', end: 'top 25%' },
tracks: [
{ selector: '.axis', from: 0, to: 0.28, easing: 'ease-out' },
{ selector: '.bar-1', from: 0.1, to: 0.42, easing: 'ease-out' },
{ selector: '.bar-2', from: 0.26, to: 0.56, easing: 'ease-out' },
{ selector: '.bar-3', from: 0.42, to: 0.72, easing: 'ease-out' },
{ selector: '.bar-4', from: 0.58, to: 0.88, easing: 'ease-out' },
// Trend line draws only after all bars are visible
{ selector: '.trend', from: 0.75, to: 1.0, easing: 'spring' },
],
});09
·scrollDrawGroup · synchronizedThree separate SVG containers — Speed, Size, and Framework icons — all animate simultaneously the moment the section scrolls into view. scrollDrawGroup wires them to the same scroll timeline with one call.
import { scrollDrawGroup } from 'svg-scroll-draw/group';
// All three containers share the same options and
// start drawing at exactly the same scroll position.
const group = scrollDrawGroup(
[speedRef.current, sizeRef.current, frameworkRef.current],
{
easing: 'ease-out',
speed: 1.1,
fade: true,
once: true,
trigger: { start: 'top 88%', end: 'top 25%' },
}
);
// One call controls all instances
group.replay();
group.pause();
group.destroy(); // cleanup on unmount10
·ScrollDraw component · useScrollDraw composableA Vue component tree animates its borders and prop-flow connections on scroll. Uses the <ScrollDraw> component — or the useScrollDraw composable for full ref control. Both are included in svg-scroll-draw/vue.
<!-- Option 1: <ScrollDraw> component -->
<script setup>
import { ScrollDraw } from 'svg-scroll-draw/vue';
</script>
<template>
<ScrollDraw easing="ease-out" :speed="0.9" fade once>
<svg viewBox="0 0 200 100" fill="none">
<path d="M10 50 Q100 10 190 50"
stroke="#42b883" stroke-width="2.5" />
</svg>
</ScrollDraw>
</template>
<!-- Option 2: useScrollDraw composable -->
<script setup>
import { useScrollDraw } from 'svg-scroll-draw/vue';
const containerRef = useScrollDraw({
easing: 'spring',
once: true,
trigger: { start: 'top 80%', end: 'center 20%' },
});
</script>
<template>
<div :ref="containerRef">
<svg viewBox="0 0 200 100" fill="none">
<path d="M10 50 Q100 10 190 50"
stroke="#42b883" stroke-width="2.5" />
</svg>
</div>
</template>11
·use:scrollDraw action · createScrollDrawA reactive store graph — $source driving three derived values — animates with spring easing on scroll. Uses the Svelte use:scrollDraw action; createScrollDraw gives access to the instance for replay/pause.
<!-- Option 1: use:scrollDraw action (simplest) -->
<script>
import { scrollDraw } from 'svg-scroll-draw/svelte';
</script>
<div use:scrollDraw={{ easing: 'spring', fade: true, once: true }}>
<svg viewBox="0 0 200 100" fill="none">
<path d="M10 50 Q100 10 190 50"
stroke="#ff3e00" stroke-width="2.5" />
</svg>
</div>
<!-- Option 2: createScrollDraw for instance control -->
<script>
import { createScrollDraw } from 'svg-scroll-draw/svelte';
const { action, getInstance } = createScrollDraw({
easing: 'spring',
once: true,
speed: 1.2,
});
</script>
<div use:action>
<svg viewBox="0 0 200 100" fill="none">
<path d="M10 50 Q100 10 190 50"
stroke="#ff3e00" stroke-width="2.5" />
</svg>
</div>
<button on:click={() => getInstance()?.replay()}>
Replay
</button>12
·useScrollDraw hook · createScrollDrawA fine-grained reactivity graph — createSignal feeding two createMemo derivations into a createEffect — animates with ease-out on scroll. Uses the useScrollDraw hook; createScrollDraw gives access to the instance for replay and pause.
// Option 1: useScrollDraw hook (simplest)
import { useScrollDraw } from 'svg-scroll-draw/solid';
function Hero() {
const ref = useScrollDraw({
easing: 'ease-out',
fade: true,
once: true,
});
return (
<div ref={ref}>
<svg viewBox="0 0 200 80" fill="none">
<path d="M10 40 Q100 5 190 40"
stroke="#446B9E" stroke-width="2.5" />
</svg>
</div>
);
}
// Option 2: createScrollDraw — instance control
import { createScrollDraw } from 'svg-scroll-draw/solid';
function HeroWithReplay() {
const { ref, getInstance } = createScrollDraw({
easing: 'spring',
once: true,
});
return (
<>
<div ref={ref}>
<svg viewBox="0 0 200 80" fill="none">
<path d="M10 40 Q100 5 190 40"
stroke="#446B9E" stroke-width="2.5" />
</svg>
</div>
<button onClick={() => getInstance()?.replay()}>
Replay
</button>
</>
);
}13
·preset option · one-liner setupFive named presets — sketch, reveal, typewriter, cinematic, spring — apply sensible defaults in a single option. Each preset is a shorthand for 2–4 common options; user options always override.
import { scrollDraw } from 'svg-scroll-draw';
// One-liner for each common pattern
scrollDraw('#logo', { preset: 'reveal' }); // fade + ease-out, once
scrollDraw('#diagram', { preset: 'sketch' }); // staggered ease-in
scrollDraw('#text', { preset: 'typewriter' }); // fast linear stagger
scrollDraw('#hero', { preset: 'cinematic' }); // slow fade ease-in-out
scrollDraw('#icon', { preset: 'spring' }); // spring easing
// Override any preset value
scrollDraw('#logo', { preset: 'reveal', easing: 'spring' });
// Inspect presets
import { PRESETS } from 'svg-scroll-draw';
console.log(PRESETS.reveal);
// { easing: 'ease-out', fade: true, speed: 1.2, once: true }14
·scrollDrawSequence · one after anotherThree steps — Code, Build, Ship — draw in strict sequence: each one starts only after the previous reaches 100%. The border and label color up as each step completes. Uses scrollDrawSequence with onComplete to track state.
import { scrollDrawSequence } from 'svg-scroll-draw/group';
// Each container starts drawing only after
// the previous one fully completes.
const seq = scrollDrawSequence(
[codeRef.current, buildRef.current, shipRef.current],
{
easing: 'ease-out',
speed: 1.4,
fade: true,
trigger: { start: 'top 88%', end: 'top 25%' },
onComplete: () => setStep((s) => s + 1),
}
);
seq.replay(); // restart from step 1
seq.destroy(); // cleanup on unmount01
Code
02
Build
03
Ship
15
·v2 · scrollAnimate · staggeredA pricing card where every element — badge, plan name, price, feature list, CTA — reveals on scroll with staggered scrollAnimate calls. Each element has its own trigger offset for a natural cascade.
import { scrollAnimate } from 'svg-scroll-draw';
// Each element gets its own trigger offset → natural cascade
scrollAnimate(badgeEl, {
props: { opacity: [0, 1], transform: ['translateY(16px)', 'translateY(0)'] },
trigger: { start: 'top 85%', end: 'top 45%' },
easing: 'ease-out', once: true,
});
scrollAnimate(priceEl, {
props: { opacity: [0, 1], transform: ['translateY(24px)', 'translateY(0)'] },
trigger: { start: 'top 80%', end: 'top 40%' }, // starts 5% later
easing: 'ease-out', once: true,
});
scrollAnimate(ctaEl, {
props: { opacity: [0, 1], transform: ['translateY(12px)', 'translateY(0)'] },
trigger: { start: 'top 74%', end: 'top 34%' }, // last to reveal
easing: 'ease-out', once: true,
});16
·v2 · scrollCounter · formattedA dark social proof section with 4 live counters — each with a different format function. Numbers count up from zero as the section scrolls into view. Cards also fade in with scrollAnimate.
import { scrollCounter, scrollAnimate } from 'svg-scroll-draw';
// Users — integer with locale formatting
scrollCounter('#users', {
to: 50_000,
format: n => Math.round(n).toLocaleString() + '+',
once: true,
});
// Satisfaction — fixed decimal percentage
scrollCounter('#satisfaction', {
to: 94.7,
format: n => n.toFixed(1) + '%',
once: true,
});
// KB size — prefix notation
scrollCounter('#size', {
to: 9,
format: n => '~' + Math.round(n),
once: true,
});
// Zero — counts down to 0 (dependencies)
scrollCounter('#deps', { to: 0, once: true });17
·v2 · scrollVideo · currentTimeTie a <video> element's currentTime to scroll position — the Apple and Stripe product-page pattern. One function call. Supports from/to in seconds, preload strategy, onReady callback, and the full pause/resume/seek/replay instance API.
import { scrollVideo } from 'svg-scroll-draw/video';
// The full Apple / Stripe product scrub pattern.
// video.currentTime is tied directly to scroll position.
scrollVideo('#hero-video', {
// Animate across the full sticky section
trigger: { start: 'top top', end: 'bottom top' },
// Optionally scrub only a clip of the video
from: 0, // seconds
to: 12.5, // seconds (defaults to video.duration)
easing: 'linear', // linear feels most natural for scrub
once: false, // reverse on scroll-up
onReady: () => console.log('metadata loaded'),
onProgress: p => progressBar.style.width = p * 100 + '%',
onComplete: () => console.log('reached end'),
});18
·v2 · scrollAnimate · staggered listFour feature rows slide in from the left on scroll with staggered scrollAnimate calls — each row has its own trigger offset for a natural cascade. The same pattern works with scrollText split: "lines" for text blocks.
import { scrollAnimate } from 'svg-scroll-draw';
// Each row has its own trigger offset — a 5% stagger
// in start position creates a natural waterfall effect.
const rows = document.querySelectorAll('.feature-row');
rows.forEach((row, i) => {
scrollAnimate(row, {
props: {
opacity: [0, 1],
transform: ['translateX(-20px)', 'translateX(0)'],
},
trigger: {
start: `top ${90 - i * 5}%`,
end: `top ${60 - i * 5}%`,
},
easing: 'ease-out',
once: true,
});
});
// Or use scrollText with split: 'lines' for text blocks:
// scrollText('#features', {
// split: 'lines', stagger: 0.08,
// from: { opacity: 0, x: -20 },
// once: true,
// });19
·v2 · scrollAnimateGroup · fan-outAnimate multiple HTML elements simultaneously with one call using scrollAnimateGroup. All four v2 API cards reveal together on scroll. Same options, same scroll timeline, zero boilerplate — the v2 parallel to scrollDrawGroup.
import { scrollAnimateGroup } from 'svg-scroll-draw/group';
// All four cards animate simultaneously — same props,
// same trigger, one call.
const group = scrollAnimateGroup(
[card1El, card2El, card3El, card4El],
{
props: {
opacity: [0, 1],
transform: ['translateY(28px)', 'translateY(0)'],
},
easing: 'ease-out',
once: true,
}
);
// Full instance API works across the entire group
group.replay(); // replay all
group.pause(); // pause all
group.destroy(); // cleanup on unmountscrollAnimateAny CSS propertyscrollCounterAnimated numbersscrollTextSplit + staggerscrollVideoScrub video20
·v2 · scrollText · words + charsA two-line marketing headline that reveals word-by-word on scroll, followed by a subtitle trickling in char by char. Each line has its own trigger window so they cascade naturally.
import { scrollText } from 'svg-scroll-draw/text';
// Line 1 — words fade up with stagger
scrollText('#line1', {
split: 'words',
stagger: 0.07,
from: { opacity: 0, y: 32 },
easing: 'ease-out',
once: true,
trigger: { start: 'top 88%', end: 'top 52%' },
});
// Line 2 — offset trigger, starts after line 1
scrollText('#line2', {
split: 'words',
stagger: 0.07,
from: { opacity: 0, y: 32 },
once: true,
trigger: { start: 'top 84%', end: 'top 48%' },
});
// Subtitle — char-by-char typewriter trickle
scrollText('#subtitle', {
split: 'chars',
stagger: 0.015,
from: { opacity: 0 },
easing: 'linear',
once: true,
});Zero deps · MIT · 9 KB · works with React, Vue, Svelte
21
·v2.8 · scrollReveal · staggerSix cards fade up and cascade into view as you scroll past — one scrollReveal call, no data attributes. The zero-config replacement for AOS and ScrollReveal.js. Scroll into the preview to see it trigger.
import { scrollReveal } from 'svg-scroll-draw/reveal';
// Fade up (default preset) with stagger
scrollReveal('.card');
// Custom from state
scrollReveal('.feature', {
from: { opacity: 0, y: 40, scale: 0.95 },
stagger: 0.1,
easing: 'ease-out',
once: true,
});
// Named presets: fadeUp | fadeDown | fadeLeft
// fadeRight | scale | flip | flipX
scrollReveal('.badge', { preset: 'scale' });
scrollReveal('.panel', { preset: 'flip' });
scrollReveal('.sidebar',{ preset: 'fadeLeft' });
// Cleanup
const instance = scrollReveal('.card');
instance.destroy();22
·v2.7 · scrollPin · onEnter / onLeaveProduct image stays pinned while feature descriptions scroll past — the Apple / Stripe product walkthrough pattern. Uses scrollPin with lifecycle callbacks. Scroll inside the preview to see the pin state change.
import { scrollPin } from 'svg-scroll-draw/pin';
// Pin the product image while features scroll past
const pin = scrollPin('#product-image', {
top: 80, // 80px below top (under a fixed nav)
pinDistance: window.innerHeight * 3,
onEnter: () => image.classList.add('active'),
onLeave: () => image.classList.remove('active'),
onEnterBack: () => image.classList.add('active'),
onLeaveBack: () => image.classList.remove('active'),
onProgress: (p) => progressBar.style.width = p * 100 + '%',
});
// Animate each feature block as it scrolls in
document.querySelectorAll('.feature').forEach(el =>
scrollAnimate(el, {
props: { opacity: [0, 1], transform: ['translateY(32px)', 'translateY(0)'] },
easing: 'ease-out', once: true,
})
);
// Recalculate after layout change (accordion, content load)
pin.refresh();
pin.destroy(); // removes pin and restores DOM23
·v2.7 · scrollSnap · horizontalDrag or swipe between cards — scrollSnap detects when you pass the threshold and smoothly animates to the nearest card with custom easing. Dot indicators update via onSnap callback.
import { scrollSnap } from 'svg-scroll-draw/snap';
// Horizontal card carousel with custom easing
const snap = scrollSnap('.card', {
direction: 'horizontal',
duration: 400,
easing: 'ease-out',
threshold: 0.3, // snap if user dragged >30% of card width
onSnap: (index) => setActiveCard(index),
});
// Vertical section snapping (fullscreen sections)
scrollSnap('.section', {
duration: 600,
easing: 'ease-in-out',
onSnap: (i) => history.replaceState(null, '', `#section-${i}`),
});
// Programmatic control
snap.snapTo(2); // jump to index 2 with animation
snap.getCurrentIndex(); // → currently snapped index
snap.destroy();