Real-world examples

SVG scroll animations
without GSAP.

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 transition

Logo Reveal

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

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

02

·selector · strokeColor

Revenue Chart

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

Hero.tsx
<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>
$80k$60k$40k$20kJanFebMarAprMayJunJul

03

·stagger · ease-out

Contract Signature

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

Hero.tsx
<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>
Authorized SignatureJ. Anderson, CEOMay 2026

04

·selector · stagger

Checkout Flow

Box borders and arrow connectors draw in sequence across a 4-step checkout process. Static fills and emoji keep the diagram readable before animation starts.

Hero.tsx
<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>
🛒Cart📦Shipping💳PaymentConfirmed01020304

05

·selector · strokeColor

Delivery Route

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

Hero.tsx
<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>
Oak StMain Ave1st StWWarehouseCCustomer2.4 km · 8 min

06

·selector · ease-out

API Architecture

Connection lines between services draw in on scroll — Browser → API Gateway → Auth / Database / Cache. Service boxes and labels are always visible; only the wires animate.

Hero.tsx
<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>
🌐 BrowserAPI Gateway🔐 Auth🗄️ Database⚡ Cache

07

·data-scroll-draw · initScrollDraw()

Astro Integration

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.

Hero.tsx
// 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>
src/pages/index.astro
<div data-scroll-draw
data-scroll-draw-options='{"easing":"ease-out","once":true}'
>
<svg viewBox="0 0 200 80">
<path d="M10 40 Q100 5 190 40"
stroke="white" fill="none" />
</svg>
</div>
<script>
import { initScrollDraw } from 'svg-scroll-draw/astro';
initScrollDraw();
</script>

08

·scrollDrawTimeline · independent tracks

Timeline API

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

Hero.tsx
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'   },
  ],
});
$0k$40k$80k$120kQ1$92kQ2$122kQ3$77kQ4$111kEach bar is an independent track
Axes
0%
Q1
0%
Q2
0%
Q3
0%
Q4
0%
Trend
0%
scroll
0%

09

·scrollDrawGroup · synchronized

Group API

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

Hero.tsx
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 unmount
Speed
~9 KB
Any framework

10

·ScrollDraw component · useScrollDraw composable

Vue 3

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

Hero.tsx
<!-- 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>
Vue 3
App.vueHeader.vueContent.vue:title="…"v-foremit('update')

11

·use:scrollDraw action · createScrollDraw

Svelte

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

Hero.tsx
<!-- 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>
Svelte
$sourcewritable$countderived$labelderived$effectautorun

12

·useScrollDraw hook · createScrollDraw

Solid.js

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

Hero.tsx
// 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>
    </>
  );
}
Solid.js
createSignalcount = 0createMemodoubledcreateMemoisEvencreateEffectDOM update

13

·preset option · one-liner setup

Presets

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

Hero.tsx
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 }
sketch
reveal
typewriter
cinematic
spring

14

·scrollDrawSequence · one after another

Sequence API

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

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

01

Code

02

Build

03

Ship

15

·v2 · scrollAnimate · staggered

Pricing Card Reveal

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

Hero.tsx
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,
});
Pro Plan
Everything you need
For teams shipping fast.
$49/mo
  • Unlimited projects
  • scrollAnimate + scrollText
  • Zero dependencies
  • MIT license

16

·v2 · scrollCounter · formatted

Social Proof Strip

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

Hero.tsx
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 });
The numbers
0+developers
~0KB gzipped
0tests green
0dependencies

17

·v2 · scrollVideo · currentTime

Product Video Scrub

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

Hero.tsx
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'),
});
scroll to scrub ↓

18

·v2 · scrollAnimate · staggered list

Feature List Reveal

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

Hero.tsx
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,
// });
Why developers choose it
Native CSS compositor path. Zero JS on Chrome.
🧩React, Vue, Svelte, Solid, Angular, Astro, Nuxt.
📦~9 KB gzipped. Zero runtime dependencies.
aria-label, aria-hidden, prefers-reduced-motion.

19

·v2 · scrollAnimateGroup · fan-out

Animate Group

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

Hero.tsx
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 unmount
🎯scrollAnimateAny CSS property
🔢scrollCounterAnimated numbers
📝scrollTextSplit + stagger
🎬scrollVideoScrub video

20

·v2 · scrollText · words + chars

Hero Headline Reveal

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

Hero.tsx
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,
});
scrollText demo

Build scroll animations.

Without GSAP.

Zero deps · MIT · 9 KB · works with React, Vue, Svelte

21

·v2.8 · scrollReveal · stagger

Card Cascade Reveal

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

Hero.tsx
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();
scrollReveal('.reveal-card', { preset: 'fadeUp', stagger: 0.12 })
🎯
scrollReveal
fadeUp preset
🌊
stagger: 0.12
🔒
once: true
🎨
7 presets
📦
~9 KB total

22

·v2.7 · scrollPin · onEnter / onLeave

Sticky Feature Panel

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

Hero.tsx
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 DOM
📌
Product Image
Stays fixed →
BEFORE
↑ Scroll the preview to see pin state
One function call
scrollPin wraps the target in a spacer — no layout shift.
Full callbacks
onEnter, onLeave, onEnterBack, onLeaveBack — same as GSAP.
Refresh on resize
Call pin.refresh() after any layout change to recalculate.

23

·v2.7 · scrollSnap · horizontal

Horizontal Snap Carousel

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

Hero.tsx
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();
📌
01 / 4
Scroll Pin
← Drag or swipe to snap →
02 / 4
Scroll Snap
← Drag or swipe to snap →
🎯
03 / 4
Callbacks
← Drag or swipe to snap →
🌊
04 / 4
Lenis
← Drag or swipe to snap →

Ready to add this
to your project?