← Loaders

Logo Draw Loader

Your logo’s outline draws itself over a faint copy of the logo, holds for a beat, then wipes away in the same direction, on repeat. A branded loader for splash screens. Works with any logo drawn with strokes.

Loading your workspace…

How to use it

  1. Paste the snippet just before </head>.
  2. Wrap your logo’s inline <svg> in a <div data-logo-draw-loader>, or put the attribute on the <svg> itself.
  3. Its shapes need a stroke (and usually fill="none").

Options

Add any of these attributes to the same element.

data-speedTime to draw each stroke. Default 1100 ms.
data-staggerGap between strokes. Default 160 ms.
data-holdHow long the finished logo stays. Default 500 ms.
data-fillShapes with a fill flood in once drawn, and empty again before the wipe.
data-ghost="false"No faint copy underneath.

It only draws while it’s on screen, and is marked as a progress bar labelled “Loading” (set your own aria-label). With reduced motion the logo simply shows.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Logo Draw Loader: your SVG logo's outline draws itself over a faint copy
    // of the logo, holds for a beat, then wipes away in the same direction, on
    // repeat. Works with any logo drawn with strokes.
    // It loops while it's on screen.
    // Usage: <div data-logo-draw-loader aria-label="Loading">
    //          <svg viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="3">…your logo…</svg>
    //        </div>
    //   (or put data-logo-draw-loader on the <svg> itself)
    // Optional: data-speed (ms to draw each stroke, default 1100), data-stagger
    // (ms between strokes, default 160), data-hold (ms the full logo stays,
    // default 500), data-fill (shapes with a fill flood in once drawn and empty
    // again before the wipe), data-ghost="false" (no faint copy underneath).
    var DEFAULTS = { speed: 1100, stagger: 160, hold: 500 };
    var SHAPES = 'path, line, polyline, polygon, circle, ellipse, rect';

    var css = '.ldl-ghost{opacity:.14}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function build(el) {
      var d = el.dataset;
      var svg = el.tagName.toLowerCase() === 'svg' ? el : el.querySelector('svg');
      if (!svg) return null;
      if (!el.getAttribute('role')) el.setAttribute('role', 'progressbar');
      if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'Loading');
      if (svg !== el) svg.setAttribute('aria-hidden', 'true');
      var shapes = Array.from(svg.querySelectorAll(SHAPES)).filter(function (s) { return !s.closest('defs, clipPath, mask'); });
      if (!shapes.length || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return null;

      if (d.ghost !== 'false') {
        // A faint copy of every stroke underneath, so the logo's shape is
        // always there to be traced over.
        shapes.forEach(function (s) {
          var g = s.cloneNode(false);
          g.setAttribute('class', ((s.getAttribute('class') || '') + ' ldl-ghost').trim());
          g.setAttribute('aria-hidden', 'true');
          s.parentNode.insertBefore(g, s);
        });
      }
      var fill = d.fill !== undefined && d.fill !== 'false';
      var filled = fill ? shapes.filter(function (s) { var f = getComputedStyle(s).fill; return f && f !== 'none'; }) : [];
      filled.forEach(function (s) { s.style.fillOpacity = 0; });

      var lines = anime.svg.createDrawable(shapes);
      anime.utils.set(lines, { draw: '0 0' });
      var speed = +d.speed || DEFAULTS.speed;
      var stagger = d.stagger !== undefined ? +d.stagger : DEFAULTS.stagger;
      var hold = d.hold !== undefined ? +d.hold : DEFAULTS.hold;
      var tl = anime.createTimeline({ loop: true, loopDelay: 250, autoplay: false });
      tl.add(lines, { draw: ['0 0', '0 1'], duration: speed, ease: 'inOut(2.5)', delay: anime.stagger(stagger) });
      if (filled.length) {
        tl.add(filled, { fillOpacity: [0, 1], duration: 450, ease: 'out(2)', delay: anime.stagger(60) }, '-=150');
        tl.add(filled, { fillOpacity: 0, duration: 350, ease: 'in(2)' }, '+=' + hold);
      }
      // Wipe off the way it was drawn, quicker, so it reads as one motion.
      tl.add(lines, { draw: ['0 1', '1 1'], duration: speed * 0.75, ease: 'inOut(2.5)', delay: anime.stagger(stagger * 0.6) },
        filled.length ? '+=0' : '+=' + hold);
      return tl;
    }

    function init() {
      document.querySelectorAll('[data-logo-draw-loader]').forEach(function (el) {
        if (el._ldl) return;
        el._ldl = true;
        var tl = build(el);
        if (!tl) return;
        // Only draw while visible, to save battery.
        new IntersectionObserver(function (entries) {
          entries[0].isIntersecting ? tl.play() : tl.pause();
        }).observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-logo-draw-loader aria-label="Loading">
  <svg viewBox="0 0 64 64" fill="none" stroke="#6a0dad" stroke-width="3.5" stroke-linecap="round">
    <circle cx="32" cy="32" r="27"/>
    <path d="M19.5 44.5 L32 17 L44.5 44.5"/>
  </svg>
</div>