Next.jsApp Router · Pages Router · SSR-safe

Scroll animations
for Next.js.

svg-scroll-draw is built SSR-first. All scroll APIs guard against window being undefined. Works in App Router Server Components, Client Components, and Pages Router — no hydration errors, no document is not defined.

$npm i svg-scroll-draw
React guide →

The patterns.

Pattern 1 — Client Component (recommended)

Add 'use client' to any component that uses scroll APIs. Everything else can be a Server Component.

components/AnimatedCards.tsx
'use client';
import { useEffect } from 'react';
import { scrollReveal } from 'svg-scroll-draw/reveal';
import { scrollAnimate } from 'svg-scroll-draw';

export function AnimatedCards({ cards }: { cards: Card[] }) {
  useEffect(() => {
    const reveal = scrollReveal('.card', { preset: 'fadeUp', stagger: 0.1 });
    const hero   = scrollAnimate('#hero-title', {
      props: { opacity: [0, 1], transform: ['translateY(32px)', 'translateY(0)'] },
      easing: 'ease-out',
      once:   true,
    });
    return () => { reveal.destroy(); hero.destroy(); };
  }, []);

  return (
    <div>
      <h1 id="hero-title">Features</h1>
      {cards.map(c => <div key={c.id} className="card">{c.content}</div>)}
    </div>
  );
}

Pattern 2 — Dynamic import (heavy animated sections)

For large animated sections, lazy-load them so they don't block the initial page render.

app/page.tsx
import dynamic from 'next/dynamic';

// This section and its scroll animations only load when needed
const AnimatedShowcase = dynamic(
  () => import('@/components/AnimatedShowcase'),
  {
    ssr:     false,
    loading: () => <div className="h-96 animate-pulse bg-gray-100" />,
  }
);

// Server Component — no 'use client' needed here
export default async function Page() {
  const data = await fetchData(); // server-side data fetching
  return (
    <main>
      <Hero />          {/* Server Component — static */}
      <AnimatedShowcase data={data} />  {/* Client, lazy */}
    </main>
  );
}

Pattern 3 — useEffect cleanup (avoid memory leaks)

components/PinnedSection.tsx
'use client';
import { useEffect, useRef } from 'react';
import { scrollPin }      from 'svg-scroll-draw/pin';
import { scrollHorizontal } from 'svg-scroll-draw/horizontal';
import { scrollReveal }   from 'svg-scroll-draw/reveal';

export function ProductWalkthrough() {
  const imageRef = useRef<HTMLDivElement>(null);
  const trackRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const instances = [
      imageRef.current && scrollPin(imageRef.current, {
        pinDistance: window.innerHeight * 3,
        onEnter: () => console.log('pinned'),
      }),
      scrollReveal('.feature-text', { preset: 'fadeUp', stagger: 0.08 }),
    ].filter(Boolean);

    return () => instances.forEach(i => i && i.destroy());
  }, []); // run once on mount

  return (
    <div style={{ display: 'flex' }}>
      <div ref={imageRef}>...</div>
      <div className="feature-text">...</div>
    </div>
  );
}

Pattern 4 — Route change cleanup

components/AnimatedPage.tsx
'use client';
import { useEffect, useRef } from 'react';
import { usePathname } from 'next/navigation';
import { scrollReveal } from 'svg-scroll-draw/reveal';

export function AnimatedPage({ children }: { children: React.ReactNode }) {
  const pathname = usePathname();
  const instanceRef = useRef<{ destroy: () => void } | null>(null);

  useEffect(() => {
    // Re-run on route change — pathname in deps array
    instanceRef.current = scrollReveal('.animate-in', {
      preset: 'fadeUp',
      stagger: 0.08,
      once: true,
    });

    return () => instanceRef.current?.destroy();
  }, [pathname]); // re-initialize on route change

  return <div>{children}</div>;
}

SSR safety

Every svg-scroll-draw engine starts with if (typeof window === 'undefined') return NOOP;. All server-side renders return a no-op instance — no hydration mismatch, no crashes.

Native CSS fast path

When the browser supports animation-timeline: view(), svg-scroll-draw automatically uses it — zero JS scroll listeners, zero rAF calls, pure compositor animation. Falls back to JS engine seamlessly.

vs common alternatives.

GSAP + ScrollTrigger

Bundle~50 KB
SSR-safe✗ needs guards
TypeScript~

Commercial license for SplitText

Framer Motion

Bundle~35 KB
SSR-safe
TypeScript

React-only. No SVG draw, no pin/snap.

svg-scroll-draw

Bundle~9 KB
SSR-safe
TypeScript

MIT. Everything. Works everywhere.

Built for Next.js.

SSR-safe by default. App Router ready. MIT. ~9 KB.

$npm i svg-scroll-draw
Full docs →