Retour au catalogue

Timeline Draw Path

Timeline verticale ou un path SVG sinueux se dessine progressivement au scroll (motion.path + pathLength lie a useScroll). Les jalons-cercles s'illuminent quand la ligne les atteint, les blocs texte font un reveal directionnel (x offset). Ambiance premium minimaliste, zero couleur tape-a-l-oeil.

timelinecomplex Both Responsive a11y
elegantminimaleditorialsaasagencyuniversalstackedcentered
Theme

How to build a scroll-driven SVG path timeline in React

A scroll-driven SVG path timeline in React works by tying Framer Motion's pathLength motion value to useScroll progress via a useSpring smoother. The path draws itself as the section enters the viewport, and each milestone dot scales in exactly when the animated stroke reaches its position on the path.

  • Stack: React + Framer Motion 11 + inline CSS tokens, ~105 lines, zero extra dependencies.
  • Core API: useScroll, useSpring, useTransform, motion.path pathLength, useInView.
  • The SVG path is computed at render time via a cubic Bézier generator (buildPath) that alternates left/right offsets to produce the wave shape.
  • Accessible: the SVG is aria-hidden, text content lives in standard DOM elements.
  • Content blocks alternate left/right with a directional x-offset reveal; works on mobile because scroll is available on all devices.

Timeline Draw Path is a vertical timeline React section where a wavy SVG stroke traces itself from top to bottom as the user scrolls. Milestone circles pop in when the animated line reaches them, and each content block slides in from its alternating side. The result feels like the story is being written in real time, which makes it a strong fit for product roadmaps, company histories, or feature launch sequences.

Anatomy

A centered header block (eyebrow, h2, subtitle) sits above the timeline body. The body is a relative container that holds two things at once: an absolutely positioned SVG rail centered horizontally, and a padded column of content rows. Each row is a flex container whose height matches the fixed STEP_H constant (180px), with the text card pushed left or right depending on the step index. The SVG contains a static gray guide path, the animated accent-colored motion.path on top of it, and the MilestoneDot group for each step.

How it works

useScroll tracks the section element from when its top hits 75% of the viewport down to when its bottom is at 30%. The raw progress feeds into useSpring (stiffness 80, damping 22) to eliminate jitter and add momentum. useTransform maps the smoothed spring value [0, 1] directly onto Framer Motion's pathLength motion value. On the SVG side, a motion.path has its pathLength style set to this value so Framer Motion handles the stroke-dashoffset math. Each MilestoneDot maps its own position (idx / total) into a [t-0.04, t+0.06] window on the same spring to scale from 0 to 1, so dots appear exactly as the stroke arrives. Text blocks use useInView with once:true for a one-shot directional reveal.

How to build it in React

  1. Generate the wavy SVG path

    Write a buildPath function that starts at the SVG midpoint and appends one cubic Bézier curve per step. Alternate the horizontal control-point offset (+8 then -8) between even and odd steps to create the wave. End with a vertical line to the bottom of the last step.

    const PX = 40; // SVG midpoint x
    const STEP_H = 180;
    
    function buildPath(n: number): string {
      if (n === 0) return "";
      let d = `M ${PX} 0`;
      for (let i = 0; i < n; i++) {
        const cy = i * STEP_H + STEP_H * 0.1;
        const py = i > 0 ? (i - 1) * STEP_H + STEP_H * 0.1 : 0;
        const mid = (py + cy) / 2;
        const off = i % 2 === 0 ? 8 : -8;
        d += ` C ${PX + off} ${mid}, ${PX - off} ${mid}, ${PX} ${cy}`;
      }
      return d + ` L ${PX} ${n * STEP_H}`;
    }
  2. Tie scroll progress to pathLength

    Attach a ref to the timeline container and pass it to useScroll with offset ['start 0.75', 'end 0.3']. Smooth the raw progress with useSpring, then map it to [0, 1] with useTransform. Pass the result as the pathLength style prop on a motion.path.

    const sectionRef = useRef<HTMLDivElement>(null);
    const { scrollYProgress } = useScroll({
      target: sectionRef,
      offset: ["start 0.75", "end 0.3"],
    });
    const smooth = useSpring(scrollYProgress, { stiffness: 80, damping: 22 });
    const pathLength = useTransform(smooth, [0, 1], [0, 1]);
    
    // In JSX:
    <motion.path d={svgPath} stroke="var(--color-accent)" style={{ pathLength }} />
  3. Animate milestone dots from the same spring

    For each dot at index idx out of total steps, compute the normalized position t = idx / total. Use useTransform on the shared smooth spring with a small window around t to drive scale and opacity from 0 to 1. The dot appears precisely when the stroke arrives.

    function MilestoneDot({ progress, idx, total }) {
      const t = idx / total;
      const dotScale = useTransform(progress, [t - 0.04, t + 0.06], [0, 1]);
      const dotOpacity = useTransform(progress, [t - 0.04, t + 0.06], [0, 1]);
      // render motion.circle with style={{ scale: dotScale, opacity: dotOpacity }}
    }
  4. Reveal content blocks with alternating direction

    Wrap each text card in a motion.div with initial={{ opacity: 0, x: isLeft ? 32 : -32 }}. Use useInView with once:true on the row ref and switch to animate={{ opacity: 1, x: 0 }} when it enters the viewport. This mirrors the left/right alternation of the path wave.

    const ref = useRef<HTMLDivElement>(null);
    const isInView = useInView(ref, { once: true, margin: "-12% 0px" });
    const isLeft = index % 2 === 0;
    
    <motion.div
      initial={{ opacity: 0, x: isLeft ? 32 : -32, y: 6 }}
      animate={isInView ? { opacity: 1, x: 0, y: 0 } : {}}
      transition={{ duration: 0.65, ease: [0.16, 1, 0.3, 1] }}
    />

When to use it

Use this component when you have a linear narrative with 4 to 6 clearly dated milestones: company history, product roadmap, onboarding flows, or release changelogs. The drawn-path metaphor signals progression intuitively. Skip it for unordered feature grids, deeply nested data, or pages where every user lands mid-scroll (the animation requires entry from the top to read correctly). Also avoid it when the content has fewer than 3 steps; the wave has no room to breathe.

Used by

  • Stripe, Uses scroll-progressive path reveals on its product story pages to guide readers through infrastructure milestones.
  • Linear, Employs scroll-tied SVG animations on its changelog and roadmap sections to communicate momentum.
  • Framer, Showcases path-drawing animations in its own marketing site as a direct demonstration of its animation capabilities.
  • Vercel, Uses scroll-driven connectors between feature blocks on infrastructure product pages to narrate a deployment story.

FAQ

Why is the path drawn as a cubic Bézier wave rather than a straight line?

The alternating curve gives the line organic movement and visually separates the left and right content blocks without requiring a two-column grid. A straight vertical line works too, but it reads as a plain divider rather than a narrative thread.

Can I change the number of steps without touching the SVG?

Yes. The buildPath function takes the step count as an argument and the SVG height is derived from steps.length * STEP_H. Pass more or fewer steps in the steps prop and the rail recomputes automatically.

The animation starts mid-way when users land on an anchor link deep in the page. How do I fix that?

useScroll computes progress from the current scroll position at mount. If the page loads pre-scrolled, the path will appear partly drawn. One fix is to reset scrollYProgress to 0 in a useEffect on mount, or to gate the section behind a scroll-to-top entry point.

How do I adapt the layout for a vertical mobile view where left/right alternation collapses?

On narrow screens, override the flex justification to always use flex-start and remove the horizontal x-offset so all cards stack on one side. The SVG rail stays centered; only the content column alignment changes. A CSS custom property or a window-width check in the component is enough.

"use client";

import { motion, useScroll, useSpring, useTransform, useInView } from "framer-motion";
import { useRef } from "react";

export interface TimelineStep {
  id: string; date: string; title: string; description: string; tag?: string;
}

interface Props {
  eyebrow?: string; title?: string; subtitle?: string; steps: TimelineStep[];
}

const EASE = [0.16, 1, 0.3, 1] as const;
const STEP_H = 180;
const PX = 40; // SVG midpoint x

function buildPath(n: number): string {
  if (n === 0) return "";
  let d = `M ${PX} 0`;
  for (let i = 0; i < n; i++) {
    const cy = i * STEP_H + STEP_H * 0.1;

Code complet réservé à Pro

Code source intégral, export multi-framework et playground.

Passer en Pro, 9,99€/mois

Reviews

React Scroll-Animated SVG Timeline, Code + Tutorial