Path Follow
Something travels along an SVG path, turning to face the way it’s going, round and round. Add several followers and they space themselves out evenly.
How to use it
- Paste the snippet just before
</head>. - Add
data-path-followto an<svg>. Its first plain<path>is the track. - Add the travelling shapes to the same SVG with
data-follower, drawn centred on 0,0 and pointing right.
Options
Add any of these attributes to the same element.
data-duration | Time for one lap. Default 4000 ms. |
data-rotate="false" | Keep followers upright instead of turning with the path. |
data-alternate | Go back and forth instead of looping. |
data-track | A selector for the track, if it isn’t the first path. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Path Follow: something travels along an SVG path, turning to face the
// way it's going, round and round. Add several and they space out evenly.
// Usage:
// <svg data-path-follow viewBox="0 0 200 120">
// <path d="M20 60 C60 0 140 120 180 60" fill="none" stroke="#ddd"/>
// <path data-follower d="M-8 -6 L10 0 L-8 6 Z" fill="#a855f7"/>
// </svg>
// The first plain <path> is the track. Draw each follower centred on 0,0,
// pointing right, and mark it data-follower (any SVG element works).
// Optional: data-duration (ms per lap, default 4000), data-rotate="false"
// (keep followers upright), data-alternate (go back and forth instead of
// looping), data-track (selector for the track, if it isn't the first path).
var DEFAULTS = { duration: 4000 };
function setup(svg) {
var d = svg.dataset;
var track = d.track ? svg.querySelector(d.track) : svg.querySelector('path:not([data-follower])');
var followers = Array.from(svg.querySelectorAll('[data-follower]'));
if (!track || !followers.length) return;
var path = anime.svg.createMotionPath(track);
var duration = +d.duration || DEFAULTS.duration;
var alternate = d.alternate !== undefined && d.alternate !== 'false';
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var anims = followers.map(function (f, i) {
var props = {
translateX: path.translateX,
translateY: path.translateY,
duration: duration,
ease: alternate ? 'inOut(2)' : 'linear',
loop: true,
alternate: alternate,
autoplay: false
};
if (d.rotate !== 'false') props.rotate = path.rotate;
var a = anime.animate(f, props);
// Space several followers evenly round the track.
a.seek((duration * i) / followers.length);
return a;
});
if (reduced) return;
new IntersectionObserver(function (entries) {
anims.forEach(function (a) { if (entries[0].isIntersecting) a.play(); else a.pause(); });
}).observe(svg);
}
function init() {
document.querySelectorAll('[data-path-follow]').forEach(function (svg) {
if (svg._pf) return;
svg._pf = true;
setup(svg);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><svg data-path-follow viewBox="0 0 200 120">
<path d="M20 60 C60 0 140 120 180 60" fill="none" stroke="#ddd"/>
<path data-follower d="M-8 -6 L10 0 L-8 6 Z" fill="#a855f7"/>
</svg>