GSAP MotionPathPlugin can animate an HTML or SVG element along an SVG path. Add ScrollTrigger and the element’s position can follow scroll progress instead of elapsed time.

This tutorial builds a complete basic version: a marker follows a curved SVG route, rotates with each turn, and leaves a drawn progress line behind it. The setup also handles reduced motion and rebuilds its alignment when the layout changes size.

What MotionPathPlugin does

MotionPathPlugin converts an SVG path into motion coordinates. Its align option places the animated element on that path even when the element and SVG use different coordinate systems. Set autoRotate: true and the element also turns to match the path tangent.

The important distinction is that the SVG path controls where the marker moves, while the GSAP tween controls when it moves. We will connect that tween to ScrollTrigger so the scrollbar becomes the timeline.

What you will build

The finished section has four parts:

  • A tall scroll container that provides animation distance
  • A sticky stage that remains visible during the journey
  • An SVG route with separate background and progress strokes
  • An HTML marker aligned to the SVG path by MotionPathPlugin

The progress stroke and marker share one GSAP timeline. That keeps them synchronized without calculating coordinates on every scroll event.

Step 1: Add GSAP, MotionPathPlugin, and ScrollTrigger

Load GSAP core followed by both plugins. MotionPathPlugin handles the route; ScrollTrigger maps it to scroll position.

<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/MotionPathPlugin.min.js"></script>

Register plugins before creating any animations:

gsap.registerPlugin(ScrollTrigger, MotionPathPlugin);

Step 2: Create the SVG path and marker

Use one path three times. The first copy is the faint route, the second is the colored progress line, and the third is invisible geometry for MotionPathPlugin.

<section class="route-scroll" data-route-scroll>
  <div class="route-stage">
    <svg
      class="route-map"
      viewBox="0 0 900 520"
      aria-hidden="true"
      focusable="false"
    >
      <path
        class="route-line"
        d="M70 430 C170 400 160 230 280 210 C410 188 455 390 590 330 C710 276 700 120 830 82"
      />
      <path
        class="route-progress"
        data-route-progress
        d="M70 430 C170 400 160 230 280 210 C410 188 455 390 590 330 C710 276 700 120 830 82"
      />
      <path
        class="route-motion"
        data-route-path
        d="M70 430 C170 400 160 230 280 210 C410 188 455 390 590 330 C710 276 700 120 830 82"
      />
    </svg>

    <span class="route-marker" data-route-marker aria-hidden="true">
      <span class="route-marker-arrow"></span>
    </span>
  </div>
</section>

All three d attributes must match. If they drift apart, the progress line and marker will follow different routes.

The motion path can remain invisible. MotionPathPlugin reads its geometry; it does not need a visible stroke.

Step 3: Make the route stage sticky

The outer section creates scroll distance. Its child stays pinned with CSS position: sticky, so this version does not need ScrollTrigger’s pin option.

* {
  box-sizing: border-box;
}

.route-scroll {
  position: relative;
  height: 350vh;
}

.route-stage {
  position: sticky;
  top: 0;
  width: 100%;
  height: 100vh;
  min-height: 560px;
  overflow: hidden;
  background: #e8eae5;
}

.route-map {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.route-line,
.route-progress,
.route-motion {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.route-line {
  stroke: rgba(21, 23, 19, 0.2);
  stroke-width: 6;
  stroke-dasharray: 2 14;
}

.route-progress {
  stroke: #7ca600;
  stroke-width: 6;
}

.route-motion {
  stroke: transparent;
  stroke-width: 1;
}

.route-marker {
  position: absolute;
  top: 0;
  left: 0;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border: 3px solid #e8eae5;
  border-radius: 50%;
  background: #151713;
}

.route-marker-arrow {
  width: 15px;
  height: 18px;
  background: #c8ff00;
  clip-path: polygon(0 0, 100% 50%, 0 100%, 28% 50%);
}

@media (prefers-reduced-motion: reduce) {
  .route-scroll {
    height: auto;
  }

  .route-stage {
    position: relative;
    height: min(75vh, 700px);
  }

  .route-marker {
    display: none;
  }
}

The marker uses stable dimensions and starts at top: 0; left: 0. MotionPathPlugin applies transforms from that known origin.

Step 4: Animate along the SVG path on scroll

First measure the progress path with getTotalLength(). Setting strokeDasharray and strokeDashoffset to that length hides the colored stroke. Animating the offset back to zero reveals it from start to finish.

The marker tween starts at the same timeline position:

gsap.registerPlugin(ScrollTrigger, MotionPathPlugin);

document.addEventListener('DOMContentLoaded', function () {
  const container = document.querySelector('[data-route-scroll]');
  const stage = container && container.querySelector('.route-stage');
  const path = container && container.querySelector('[data-route-path]');
  const progressPath = container && container.querySelector('[data-route-progress]');
  const marker = container && container.querySelector('[data-route-marker]');

  if (!container || !stage || !path || !progressPath || !marker) return;

  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
    gsap.set(progressPath, {
      strokeDasharray: 'none',
      strokeDashoffset: 0
    });
    return;
  }

  let timeline;
  let trigger;
  let resizeCall;

  function build(progress) {
    if (trigger) trigger.kill();
    if (timeline) timeline.kill();

    gsap.set(marker, { clearProps: 'transform' });

    const length = progressPath.getTotalLength();
    gsap.set(progressPath, {
      strokeDasharray: length,
      strokeDashoffset: length
    });

    timeline = gsap.timeline({ paused: true });

    timeline.to(progressPath, {
      strokeDashoffset: 0,
      duration: 1,
      ease: 'none'
    }, 0);

    timeline.to(marker, {
      duration: 1,
      ease: 'none',
      motionPath: {
        path: path,
        align: path,
        alignOrigin: [0.5, 0.5],
        autoRotate: true
      }
    }, 0);

    trigger = ScrollTrigger.create({
      trigger: container,
      start: 'top top',
      end: 'bottom bottom',
      animation: timeline,
      scrub: 0.8
    });

    timeline.progress(progress || 0);
  }

  build(0);

  const observer = new ResizeObserver(function () {
    if (resizeCall) resizeCall.kill();

    resizeCall = gsap.delayedCall(0.15, function () {
      const progress = trigger ? trigger.progress : 0;
      build(progress);
      ScrollTrigger.refresh();
    });
  });

  observer.observe(stage);
});

The timeline does not need a conventional duration because ScrollTrigger maps its normalized progress from 0 to 1 across the section.

scrub: 0.8 adds a short catch-up time. Use scrub: true if you want the marker to follow scroll position without smoothing.

Make the GSAP MotionPath responsive

An SVG with a viewBox scales cleanly, but MotionPath alignment is calculated when the tween initializes. Resizing the layout does not automatically rerun those calculations.

That is why the example uses ResizeObserver. When the sticky stage changes size, it:

  1. Stores the current ScrollTrigger progress
  2. Kills the old trigger and timeline
  3. Recreates MotionPath alignment at the new size
  4. Restores the saved progress
  5. Refreshes ScrollTrigger measurements

The delayed call prevents a burst of rebuilds while the browser is actively resizing.

For a composition that changes substantially on mobile, use separate SVG paths inside gsap.matchMedia() rather than forcing one path to serve every aspect ratio. The Scroll Path Journey effect includes this breakpoint-specific setup.

Common MotionPath alignment problems

The marker jumps before moving

Give the marker a known position with position: absolute; top: 0; left: 0. Then let align and alignOrigin place it on the route.

The marker is offset from the line

Use alignOrigin: [0.5, 0.5] to place the marker’s center on the path. If the visible artwork is not centered inside its element, adjust the origin or fix the asset’s internal spacing.

The route and progress line separate

The visible progress path and invisible motion path must have matching d values and share the same SVG coordinate system.

The marker drifts after resizing

Rebuild the MotionPath tween after the SVG changes dimensions. Calling only ScrollTrigger.refresh() recalculates trigger positions, but it does not recreate MotionPath alignment.

The animation is blank without JavaScript

Do not hide the base route or content in CSS. Only the colored progress stroke should be initialized by JavaScript. This preserves a useful static state if the script fails.

When to use an SVG motion path

Motion paths work well when the route carries meaning:

  • Product stories with milestones
  • Company or project timelines
  • Travel itineraries and destination guides
  • Process diagrams
  • Data stories with geographic or conceptual movement

For a decorative line that only needs to draw itself, a marker may add unnecessary complexity. Use the SVG Line Draw effect or the native stroke technique instead.

For general entrance animations, MotionPathPlugin is usually more than you need. The decision framework in Animate on Scroll: 3 Approaches Compared covers simpler options.

GSAP MotionPath FAQ

Can GSAP animate an HTML element along an SVG path?

Yes. The animated target can be an HTML element while the route is an SVG path. align converts between their coordinate systems and alignOrigin controls which point on the HTML element sits on the path.

How do you connect MotionPathPlugin to ScrollTrigger?

Create a GSAP tween or timeline containing the motionPath animation, then pass that animation to ScrollTrigger.create(). Set scrub to connect timeline progress to scroll progress.

Does MotionPathPlugin require a visible SVG line?

No. The route path can use a transparent stroke. A separate duplicate path can provide the visual route or progress line.

How do you make a MotionPath animation responsive?

Use an SVG viewBox, keep the route and animated stage in stable coordinate systems, and rebuild the MotionPath tween when the rendered size changes. Use separate paths at major layout breakpoints when one composition cannot fit both desktop and mobile.

The production version

This tutorial covers a complete single-route animation. The Scroll Path Journey adds the integration features needed for more complex storytelling:

  • Separate desktop and mobile routes through gsap.matchMedia()
  • Checkpoints activated by exact route progress
  • Clickable checkpoint navigation
  • journey:checkpoint events for media and application state
  • Configurable scrub, trigger positions, rotation, and active classes
  • Reusable multi-instance setup with full observer, event, tween, and ScrollTrigger cleanup
  • A reduced-motion composition that removes the extended scroll range

View the full effect with all options ->