GuideMay 2026 · 10 min read

SVG Animation on Scroll — CSS, Intersection Observer & GSAP

Scroll-triggered SVG animations are one of the most effective ways to add polish to a landing page. Here are three techniques — from pure CSS to full GSAP — with copy-paste code for each.

Why Trigger SVG Animations on Scroll?

An SVG animation that plays immediately on page load is easy to miss — especially if it is below the fold. SVG animation on scroll solves this by holding the animation paused until the element enters the viewport, then playing it once the user reaches it. This is particularly effective for:

  • Logo draw-on animations in hero sections
  • Path animations in feature diagrams
  • Icon animations in feature lists
  • Chart or counter animations in stats sections

Technique 1 — Pure CSS (animation-play-state)

The simplest approach uses CSS animation-play-state: paused by default, then adds an .is-visible class via a tiny JavaScript observer to resume it. The CSS does all the heavy lifting — no animation library needed.

/* CSS: animation starts paused */
.draw-path {
  stroke-dasharray: 480;
  stroke-dashoffset: 480;
  animation: drawOn 1.6s ease-out forwards paused;
}

/* Resume when class is added */
.draw-path.is-visible {
  animation-play-state: running;
}

@keyframes drawOn {
  to { stroke-dashoffset: 0; }
}
// JS: observe every .draw-path element
const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        entry.target.classList.add("is-visible");
        observer.unobserve(entry.target); // play once
      }
    });
  },
  { threshold: 0.2 } // 20% visible = trigger
);

document.querySelectorAll(".draw-path").forEach((el) => observer.observe(el));

Technique 2 — Intersection Observer API (Full Control)

For more control — like replaying on re-entry, adjusting timing dynamically, or coordinating multiple elements — drive the animation entirely from JavaScript using the Intersection Observer API. This approach pairs well with SVG exported from a visual editor.

const svgPaths = document.querySelectorAll("[data-draw]");

svgPaths.forEach((path) => {
  const length = path.getTotalLength();
  path.style.strokeDasharray = length;
  path.style.strokeDashoffset = length;
  path.style.transition = "none";
});

const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach(({ target, isIntersecting }) => {
      const length = target.getTotalLength();
      if (isIntersecting) {
        target.style.transition = "stroke-dashoffset 1.5s ease-out";
        target.style.strokeDashoffset = "0";
      } else {
        // Reset for replay on re-entry (remove this block to play once)
        target.style.transition = "none";
        target.style.strokeDashoffset = length;
      }
    });
  },
  { threshold: 0.15 }
);

svgPaths.forEach((path) => observer.observe(path));

Mark paths in your SVG markup with data-draw to opt them into the animation:

<path data-draw d="M10 50 Q80 10 150 50" stroke="#6366f1" fill="none" stroke-width="3" />

Technique 3 — GSAP ScrollTrigger

GSAP's ScrollTrigger plugin is the gold standard for production scroll animations. It handles scrubbing (animation progress tied to scroll position), pin-and-play sequences, and staggered multi-element timelines — all with excellent performance.

import gsap from "gsap";
import ScrollTrigger from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);

const path = document.querySelector("#my-path");
const length = path.getTotalLength();

// Set initial hidden state
gsap.set(path, { strokeDasharray: length, strokeDashoffset: length });

// Scrub: animation tied to scroll position
gsap.to(path, {
  strokeDashoffset: 0,
  scrollTrigger: {
    trigger: "#my-path",
    start: "top 80%",
    end: "top 30%",
    scrub: true,       // ties animation to scroll position
  },
});

When to use GSAP: Choose GSAP when you need scroll-scrubbed animations (progress locked to scroll position), complex staggered sequences, or pinned sections. For simple play-on-enter animations, the Intersection Observer approach is sufficient and adds zero bundle weight.

SVG Fade-In on Scroll (No Stroke Trick Needed)

Not every scroll animation needs to be a draw-on effect. A simple opacity + translate fade works for any SVG element — icons, illustrations, charts — and is GPU-composited for smooth performance:

.svg-fade {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}

.svg-fade.is-visible {
  opacity: 1;
  transform: translateY(0);
}

Stagger multiple elements by adding an nth-child delay:

.feature-icon:nth-child(1) { transition-delay: 0s; }
.feature-icon:nth-child(2) { transition-delay: 0.1s; }
.feature-icon:nth-child(3) { transition-delay: 0.2s; }

Using SVG Animations on Scroll in React

In React, the useEffect hook is the natural place to set up Intersection Observer. Here is a reusable hook:

import { useEffect, useRef, useState } from "react";

export function useInView(threshold = 0.2) {
  const ref = useRef<SVGPathElement>(null);
  const [inView, setInView] = useState(false);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const obs = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setInView(true);
          obs.disconnect(); // play once
        }
      },
      { threshold }
    );
    obs.observe(el);
    return () => obs.disconnect();
  }, [threshold]);

  return { ref, inView };
}

// Usage:
function DrawOnPath() {
  const { ref, inView } = useInView();
  return (
    <path
      ref={ref}
      d="M10 50 Q80 10 150 50"
      stroke="#6366f1"
      fill="none"
      strokeDasharray={480}
      strokeDashoffset={inView ? 0 : 480}
      style={{ transition: "stroke-dashoffset 1.5s ease-out" }}
    />
  );
}

Accessibility: Respecting Reduced Motion

Always check prefers-reduced-motion before running scroll animations. Users with vestibular disorders or motion sensitivity can be harmed by unexpected motion:

// JS guard before creating observer
const prefersReduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!prefersReduced) {
  // set up IntersectionObserver
}

/* CSS alternative: only animate if user hasn't opted out */
@media (prefers-reduced-motion: no-preference) {
  .draw-path { animation: drawOn 1.6s ease-out forwards paused; }
  .draw-path.is-visible { animation-play-state: running; }
}

Build Your SVG Animation Visually

Create the SVG in CSSVG editor, export as CSS keyframes or SMIL, then attach the scroll trigger with the Intersection Observer pattern above. Free, no sign-up required.

Open the Editor — it's free