# How to animate an element along an SVG path with GSAP

> Animate an HTML element along an SVG path on scroll using GSAP MotionPathPlugin and ScrollTrigger, with responsive alignment and reduced motion support.

Published 2026-07-17 by Jake. Canonical: https://gsapvault.com/blog/animate-element-along-svg-path-gsap

---
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](https://gsap.com/docs/v3/Plugins/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](https://gsap.com/docs/v3/Plugins/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.

```html
<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:

```javascript
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.

```html
<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.

```css
* {
  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:

```javascript
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](/effects/scroll-path-journey) 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](/effects/svg-line-draw) 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](/blog/how-to-animate-on-scroll) 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](/effects/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 ->](/effects/scroll-path-journey)