← SVG & icons

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

  1. Paste the snippet just before </head>.
  2. Add data-path-follow to an <svg>. Its first plain <path> is the track.
  3. 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-durationTime for one lap. Default 4000 ms.
data-rotate="false"Keep followers upright instead of turning with the path.
data-alternateGo back and forth instead of looping.
data-trackA selector for the track, if it isn’t the first path.
1 · Snippet
<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>
2 · Your HTML
<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>