← SVG & icons

Trim Path

A stroke segment races along a path, its head pulling ahead so it grows, then its tail catching up so it shrinks, like After Effects’ trim paths. On a closed shape it keeps going round; on an open line it sweeps from end to end.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-trim-path to an inline <svg> drawn with strokes. Every shape in it gets its own segment.
  3. Round line caps (stroke-linecap="round") make the segment look its best.

Options

Add any of these attributes to the same element.

data-durationTime for one sweep. Default 1600 ms.
data-lagHow far the tail trails the head, from 0 to 0.9 of a sweep. Default 0.4.
data-staggerGap between shapes. Default 220 ms.
data-trackOpacity of the faint path underneath; 0 hides it. Default 0.14.
data-onceOne sweep that grows into the whole path and stays.

Handy as a loading spinner on a circle or an infinity loop. It pauses while off screen, and with reduced motion the paths simply show.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Trim Path: a stroke segment races along an SVG path, its head pulling
    // ahead so it grows, then its tail catching up so it shrinks, like After
    // Effects' trim paths. On a closed shape it keeps going round; on an open
    // line it sweeps from one end to the other.
    // Usage:
    //   <svg data-trim-path viewBox="0 0 120 120" fill="none" stroke="#8b5cf6" stroke-width="6" stroke-linecap="round">
    //     <circle cx="60" cy="60" r="44"/>
    //   </svg>
    // Optional: data-duration (ms per sweep, default 1600), data-lag (how far
    // the tail trails the head, 0 to 0.9 of a sweep, default 0.4), data-stagger
    // (ms between shapes, default 220), data-track (opacity of the faint path
    // underneath, default 0.14; 0 hides it), data-once (one sweep, then it
    // stays drawn).
    var DEFAULTS = { duration: 1600, lag: 0.4, stagger: 220, track: 0.14 };
    var SHAPES = 'path, line, polyline, polygon, circle, ellipse, rect';

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    // Ease out for the head and ease in for the tail: that's what makes it stretch.
    function easeOut(t) { return 1 - Math.pow(1 - t, 3); }
    function easeIn(t) { return t * t * t; }
    function clamp(t) { return t < 0 ? 0 : t > 1 ? 1 : t; }

    function closed(s) {
      var tag = s.tagName.toLowerCase();
      if (tag === 'circle' || tag === 'ellipse' || tag === 'rect' || tag === 'polygon') return true;
      return tag === 'path' && /z\s*$/i.test(s.getAttribute('d') || '');
    }

    function setup(svg) {
      var d = svg.dataset;
      var track = d.track !== undefined ? +d.track : DEFAULTS.track;
      var shapes = Array.prototype.filter.call(svg.querySelectorAll(SHAPES), function (s) {
        return !s.closest('defs, clipPath, mask, .trp-track') && typeof s.getTotalLength === 'function';
      });
      if (!shapes.length) return null;
      var items = shapes.map(function (s) {
        if (track > 0) {
          var t = s.cloneNode(false);
          t.removeAttribute('id');
          t.setAttribute('class', 'trp-track');
          t.setAttribute('aria-hidden', 'true');
          t.style.opacity = track;
          s.parentNode.insertBefore(t, s);
        }
        return { el: s, L: s.getTotalLength(), loop: closed(s), turn: 0 };
      });
      return { svg: svg, items: items };
    }

    // Show the stretch of the path between head and tail (fractions 0..1),
    // turned round by `turn` on closed shapes.
    function place(it, tail, head) {
      var L = it.L, len = (head - tail) * L;
      var s = it.el.style;
      if (len < 0.5) { s.opacity = 0; return; }
      s.opacity = 1;
      // A dash pattern exactly one lap long wraps neatly round a closed shape.
      s.strokeDasharray = len + ' ' + (it.loop ? L - len : L * 2);
      s.strokeDashoffset = -((tail + it.turn) % 1) * L;
    }

    function run(o) {
      var d = o.svg.dataset;
      var dur = +d.duration || DEFAULTS.duration;
      var lag = Math.max(0, Math.min(0.9, d.lag !== undefined ? +d.lag : DEFAULTS.lag));
      var stagger = d.stagger !== undefined ? +d.stagger : DEFAULTS.stagger;
      var once = d.once !== undefined && d.once !== 'false';
      var visible = false, raf = 0, clock = 0, last = 0;
      // One cycle: the head sweeps over the first part, the tail over the last.
      var span = 1 + lag;
      var frame = function (now) {
        raf = 0;
        if (!visible) return;
        clock += Math.min(50, now - (last || now));
        last = now;
        var running = false;
        o.items.forEach(function (it, i) {
          var t = (clock - i * stagger) / (dur * span);
          if (t < 0) { place(it, 0, 0); running = true; return; }
          var lap = Math.floor(t), f = t - lap;
          if (once && lap > 0) {
            // Grow to the full path and stay there.
            it.el.style.strokeDasharray = 'none';
            it.el.style.opacity = 1;
            return;
          }
          running = true;
          // Closed shapes start each lap a little further round, so it never repeats on the same spot.
          if (it.loop) it.turn = (lap * 0.31) % 1;
          var head = easeOut(clamp(f * span));
          var tail = once ? 0 : easeIn(clamp(f * span - lag));
          place(it, tail, head);
        });
        if (running) raf = requestAnimationFrame(frame);
      };
      o.items.forEach(function (it) { place(it, 0, 0); });
      new IntersectionObserver(function (entries) {
        visible = entries[0].isIntersecting;
        last = 0;
        if (visible && !raf) raf = requestAnimationFrame(frame);
      }).observe(o.svg);
    }

    function init() {
      document.querySelectorAll('[data-trim-path]').forEach(function (svg) {
        if (svg._trp) return;
        var o = setup(svg);
        svg._trp = o || true;
        if (!o || reduced()) return;
        run(o);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<svg data-trim-path viewBox="0 0 120 120" width="64" fill="none"
     stroke="#8b5cf6" stroke-width="8" stroke-linecap="round">
  <circle cx="60" cy="60" r="44"/>
</svg>