Typed React components and hooks for every scroll animation pattern — fade, slide, pin, snap, counter, text reveal, video scrub, horizontal sections. No GSAP. No Framer Motion. No config files.
'use client';
import { useEffect } from 'react';
import { scrollReveal } from 'svg-scroll-draw/reveal';
export function FeaturesSection() {
useEffect(() => {
const inst = scrollReveal('.feature-card', {
preset: 'fadeUp',
stagger: 0.1,
once: true,
});
return () => inst.destroy();
}, []);
return (
<section>
{features.map(f => (
<div key={f.id} className="feature-card">{f.content}</div>
))}
</section>
);
}'use client';
import { useEffect, useRef } from 'react';
import { scrollAnimate } from 'svg-scroll-draw';
// Or use the component wrapper:
import { ScrollAnimate } from 'svg-scroll-draw/react';
// Hook style (imperative)
export function HeroSection() {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!ref.current) return;
const inst = scrollAnimate(ref.current, {
props: {
opacity: [0, 1],
transform: ['translateY(40px)', 'translateY(0)'],
},
easing: 'ease-out',
once: true,
});
return () => inst.destroy();
}, []);
return <div ref={ref}>Hero content</div>;
}
// Component style (declarative)
export function HeroDeclarative() {
return (
<ScrollAnimate
props={{ opacity: [0, 1], transform: ['translateY(24px)', 'translateY(0)'] }}
easing="ease-out"
once
>
<div>Hero content</div>
</ScrollAnimate>
);
}'use client';
import { useEffect, useRef } from 'react';
import { scrollPin } from 'svg-scroll-draw/pin';
export function ProductSection() {
const imageRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!imageRef.current) return;
const inst = scrollPin(imageRef.current, {
top: 80, // below fixed header
pinDistance: window.innerHeight * 3,
onEnter: () => setActive(true),
onLeave: () => setActive(false),
});
return () => inst.destroy();
}, []);
return (
<div style={{ display: 'flex', gap: 64 }}>
<div ref={imageRef} style={{ width: '50%' }}>
<img src="/product.webp" alt="Product" />
</div>
<div style={{ flex: 1 }}>
{features.map(f => <FeatureBlock key={f.id} {...f} />)}
</div>
</div>
);
}import { ScrollCounter } from 'svg-scroll-draw/react';
export function StatsSection() {
return (
<div className="stats-grid">
<ScrollCounter
to={50000}
format={n => Math.round(n).toLocaleString() + '+'}
easing="ease-out"
once
/>
<ScrollCounter to={9} format={n => '~' + Math.round(n) + ' KB'} once />
</div>
);
}import { ScrollText } from 'svg-scroll-draw/react';
export function Headline() {
return (
<ScrollText
split="words"
stagger={0.07}
from={{ opacity: 0, y: 32 }}
easing="ease-out"
once
>
<h1>Animate everything on scroll.</h1>
</ScrollText>
);
}scrollRevealsvg-scroll-draw/revealReveal elements on scroll. 7 presets, stagger, custom from state. Replaces AOS.
ScrollAnimatesvg-scroll-draw/reactAnimate any CSS property driven by scroll. Component wrapper around scrollAnimate.
useScrollDrawhooksvg-scroll-draw/reactHook to attach scroll-driven SVG path animation to a ref.
ScrollTextsvg-scroll-draw/reactSplit text + stagger reveal. words, chars, or lines. Replaces GSAP SplitText.
ScrollCountersvg-scroll-draw/reactAnimated number counter driven by scroll. Format function, decimals, easing.
scrollPinsvg-scroll-draw/pinPin an element at a viewport position while content scrolls past it. Full lifecycle callbacks.
scrollSnapsvg-scroll-draw/snapSection snapping with custom easing. snapTo(), getCurrentIndex(), onSnap.
scrollProgresssvg-scroll-draw/progressExpose scroll progress as --scroll-progress CSS custom property. Drive CSS with calc().
scrollHorizontalsvg-scroll-draw/horizontalApple-style horizontal scroll sections driven by vertical scroll.
svg-scroll-draw is fully SSR-safe. All engines guard against window being undefined. In Next.js App Router, add 'use client' to any component that calls scroll APIs — or use dynamic(() => import(...), { ssr: false } ) to lazy-load entire animated sections.
import dynamic from 'next/dynamic';
// Option A: 'use client' component
// AnimatedHero.tsx starts with 'use client'
import { AnimatedHero } from '@/components/AnimatedHero';
// Option B: dynamic import with ssr: false
const AnimatedSection = dynamic(
() => import('@/components/AnimatedSection'),
{ ssr: false }
);
export default function Page() {
return (
<>
<AnimatedHero />
<AnimatedSection />
</>
);
}MIT. Zero deps. TypeScript. 423 tests. Works everywhere React works.