SVG Path Follow
An 8 px radius dot that travels along an SVG curve at constant speed, one lap in 5.5 s whatever the screen's refresh rate: getTotalLength() and getPointAtLength() every frame, any path possible — 1.6 KB of JavaScript, paused off screen, placed at the arrival point when the user reduces motion.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Transitions has 52 effects, including 9 free. Explore the category →
3 usage examples



How it works
The .md-path-stage frame (at least 300 px tall, background #0a0a0f) holds .md-path-follow and an SVG with viewBox="0 0 400 250" that takes the full width. Two elements inside: the .md-path-trail curve, two quadratic Bézier arcs (M 30 200 Q 100 30 200 125 Q 300 220 370 50), drawn in 2 px indigo #6366f1 at 30% opacity; and the .md-path-dot point, an 8 px radius circle of the same color, with a glow (6 px drop-shadow). The dot is placed at the start of the curve in the HTML (cx="30" cy="200"): without a script, it is already in place.
The script walks through every .md-path-follow on the page. It measures the curve once (getTotalLength(): 471 units for the supplied path), then, each frame, computes progress from the clock — ((t - start) / duration) % 1, with duration = 5500 ms — and asks the browser for the point at that distance along the path (getPointAtLength()), whose x and y it copies into cx and cy. Since distance is measured on the curve itself, speed is constant in bends as on straight lines: about 86 units per second. Measured in Chromium 151, Firefox 153 and WebKit 26: one lap in 5,500 ms in all three, whether the screen shows 60 or 120 frames per second.
An IntersectionObserver stops the loop when the block leaves the screen and restarts it when it comes back, where the dot had stopped: measured, the dot no longer moves out of view, nothing runs for nothing. The path is free: replace the d attribute with any path (exported from a drawing tool, a road, a network), the dot will follow it. The frame has its own class, prefixed like the rest of the effect: two effects from the library pasted on the same page do not overwrite each other.
Accessibility
- Reduced motion: under
prefers-reduced-motion: reduce, the script does not start the loop and puts the dot at the end of the path. Measured in Chromium 151, Firefox 153 and WebKit 26: dot fixed at (370, 50), at the arrival point, at both readings. - Stopping: the loop runs endlessly while the block is visible. If the journey is decoration, a loop is fine; if it stands for a real movement (a parcel, a vehicle), play it once and hold the arrival (see the FAQ), or offer a pause button: motion lasting more than 5 seconds must be stoppable.
- Screen readers: the SVG has no title or description, and the dot says nothing about its position. If the journey carries information ("ship halfway across"), write it as text next to it, in a
role="status"element updated at key steps, and hide the drawing (aria-hidden="true"on the SVG). - Contrast: the indigo dot measures 4.4:1 on the
#0a0a0fbackground; the curve at 30% opacity is deliberately discreet (about 1.4:1): if the path must be read, raise its opacity to 0.6.
Browser compatibility
SVG + 1.6 KB of JavaScript (getTotalLength(), getPointAtLength(), requestAnimationFrame, IntersectionObserver), no dependency. Measured in Chromium 151, Firefox 153 and WebKit 26: same lap duration (5,500 ms), same stop off screen, same placement at the arrival point under reduced motion. Both path measuring methods have existed in every browser for more than ten years.
Without IntersectionObserver, the loop starts on load and never stops. Without JavaScript, the path and the dot show, the dot still at the start of the curve.
The code
HTML structure to paste into your page (the CSS + JS come with Premium):
<div class="md-path-stage">
<div class="md-path-follow">
<svg viewBox="0 0 400 250">
<path class="md-path-trail" d="M 30 200 Q 100 30 200 125 Q 300 220 370 50"></path>
<circle class="md-path-dot" r="8" cx="30" cy="200"></circle>
</svg>
</div>
</div>
The full HTML + CSS + JS of SVG Path Follow, ready to paste — plus all 840 effects. Or $299 once, lifetime access.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Path (SVG — d attribute of .md-path-trail) |
two quadratic arcs in a 400 × 250 viewBox |
Any SVG path works: paste one from a drawing tool. Keep the matching viewBox; the dot adjusts by itself, the length is measured on load. |
Lap duration (JS — var duration = 5500) |
5.5 s, whatever the screen's refresh rate | For a fixed speed rather than a fixed duration, compute duration = length / speed (e.g. length / 0.1 for 100 units per second): a path twice as long will take twice as long. |
One-way journey (JS — % 1) |
loop: the dot starts over | Replace it with Math.min(1, (t - start) / duration) and stop the loop when progress reaches 1: the dot stops at the arrival point and stays there. |
Dot (SVG + CSS — .md-path-dot) |
8 px radius circle, #6366f1, 6 px glow |
r="12" for a touch-friendly marker; an icon (<g> or <use>) moves with transform="translate(x y)" instead of cx / cy. |
Stroke (CSS — .md-path-trail) |
2 px, indigo, 30% opacity | stroke-dasharray: 7 6 for a dashed sea route; stroke-linecap: round and 6 px for a pipe. |
FAQ
animate(), replace ((t - start) / duration) % 1 with Math.min(1, (t - start) / duration), and only call requestAnimationFrame again while progress is below 1. The dot stops at the end of the path and stays there; to replay, set start back to null and restart the loop. That is what the three examples do.offset-path: path('…') with an offset-distance animation makes an HTML element follow a curve, with no script. But the path there is expressed in the element's pixels: it does not follow the scaling of a viewBox, and the drawn curve and the dot drift apart as soon as the SVG changes size. Here the dot lives in the same SVG as the path: they stay aligned at every size.IntersectionObserver cancels the loop as soon as the block leaves the screen (measured: the dot stops moving) and restarts it on return, picking up where the dot had stopped. Background tabs suspend requestAnimationFrame anyway.