← SVG & icons

Logo Sting

A short, punchy logo intro. A flash of light bursts out, the logo pops in with an overshoot and a little twist, a shockwave ring and rays of light fly outwards, sparks scatter, and it all settles in about a second.

How to use it

  1. Paste the snippet just before </head>.
  2. Wrap your logo (an <img>, an inline <svg>, a wordmark or all of them) in an element with data-logo-sting. Its children sit in a row, centred.
  3. Set data-color to your brand colour for the flash, ring and sparks.

Options

Add any of these attributes to the same element.

data-colorColour of the flash, ring and sparks. Default the text colour.
data-sparksHow many sparks scatter. Default 10.
data-loopSting in, hold, bow out and repeat.
data-holdHow long it shows, when looping. Default 2600 ms.
data-delayWait before it starts.

It plays when scrolled into view; animacious.logoSting(el) plays it again, say on a splash screen. The light effects are hidden from screen readers. 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 Sting: a short, punchy logo intro. A flash of light bursts out, the
    // logo pops in with an overshoot and a little twist, a shockwave ring and
    // rays of light fly outwards, sparks scatter, and it all settles. Loop it
    // and the logo bows out and stings in again.
    // Usage: <div data-logo-sting><img src="/logo.svg" alt="Your brand"></div>
    // The children sit in a row, centred; it works with an <img>, an inline
    // <svg>, a wordmark or several of them.
    // Optional: data-color (flash, ring and spark colour, default the text
    // colour), data-sparks (number of sparks, default 10), data-delay (ms before
    // it starts), data-loop (sting, hold, bow out and repeat), data-hold (ms it
    // shows when looping, default 2600).
    // Script use: animacious.logoSting(el) plays it again.
    var DEFAULTS = { sparks: 10, hold: 2600, rays: 12 };

    var css =
      '.lst{position:relative;display:inline-grid;place-items:center}' +
      '.lst-logo{display:inline-flex;align-items:center;justify-content:center;gap:inherit;position:relative;z-index:1}' +
      '.lst-fx{position:absolute;left:50%;top:50%;width:0;height:0;pointer-events:none;z-index:0}' +
      '.lst-fx>*,.lst-arm>*{position:absolute;left:0;top:0;opacity:0}' +
      '.lst-fx>.lst-arm{opacity:1;width:0;height:0}' +
      '.lst-flash{border-radius:50%;background:radial-gradient(closest-side,#fff,var(--lst-c) 38%,transparent);mix-blend-mode:screen}' +
      '.lst-ring{border-radius:50%;border:3px solid var(--lst-c)}' +
      '.lst-ray{height:2px;margin-top:-1px;border-radius:2px;background:linear-gradient(90deg,transparent,var(--lst-c),#fff);transform-origin:0 50%}' +
      '.lst-spark{width:5px;height:5px;margin:-2.5px 0 0 -2.5px;border-radius:50%;background:#fff;box-shadow:0 0 6px 1px var(--lst-c)}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

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

    function span(cls, parent) {
      var s = document.createElement('span');
      s.className = cls;
      parent.appendChild(s);
      return s;
    }

    function setup(el) {
      var d = el.dataset;
      el.classList.add('lst');
      var logo = document.createElement('span');
      logo.className = 'lst-logo';
      while (el.firstChild) logo.appendChild(el.firstChild);
      var fx = document.createElement('span');
      fx.className = 'lst-fx';
      fx.setAttribute('aria-hidden', 'true');
      el.appendChild(fx);
      el.appendChild(logo);
      if (d.color) el.style.setProperty('--lst-c', d.color);
      else el.style.setProperty('--lst-c', 'currentColor');
      var o = { el: el, logo: logo, fx: fx, rays: [], sparks: [] };
      o.flash = span('lst-flash', fx);
      o.ring = span('lst-ring', fx);
      for (var i = 0; i < DEFAULTS.rays; i++) o.rays.push(span('lst-ray', span('lst-arm', fx)));
      var n = d.sparks !== undefined ? Math.max(0, +d.sparks) : DEFAULTS.sparks;
      for (var k = 0; k < n; k++) o.sparks.push(span('lst-spark', fx));
      return o;
    }

    function sting(o) {
      var w = o.logo.offsetWidth, h = o.logo.offsetHeight;
      var rx = Math.max(w, h) / 2, ry = h / 2;
      // The flash and ring are sized to the logo: wide for a wordmark, round for a mark.
      var fw = w + h * 1.6, fh = h * 2.6, rs = h * 1.5;
      anime.utils.set(o.flash, { width: fw, height: fh, marginLeft: -fw / 2, marginTop: -fh / 2 });
      anime.utils.set(o.ring, { width: rs, height: rs, marginLeft: -rs / 2, marginTop: -rs / 2 });
      var tl = anime.createTimeline();
      tl.add(o.logo, { opacity: [0, 1], scale: [0.15, 1.16], rotate: [-10, 3], duration: 300, ease: 'out(3)' }, 70)
        .add(o.logo, { scale: 1, rotate: 0, duration: 900, ease: 'outElastic(1, .45)' }, 370)
        .add(o.flash, { opacity: [{ from: 0, to: 1, duration: 110, ease: 'out(2)' }, { to: 0, duration: 560, ease: 'out(2)' }], scale: [{ from: 0.2, to: 1, duration: 200, ease: 'out(3)' }, { to: 1.25, duration: 470, ease: 'out(2)' }] }, 0)
        .add(o.ring, { opacity: [0.9, 0], scaleX: [0.4, Math.max(2.4, (w / rs) * 1.25)], scaleY: [0.4, 2.4], duration: 800, ease: 'out(3)' }, 120);
      o.rays.forEach(function (ray, i) {
        // Each ray sits in a wrapper turned to its angle, and shoots out along it
        // from just outside the logo's (elliptical) edge.
        var a = (i / o.rays.length) * Math.PI * 2 + 0.13;
        var c = Math.cos(a), s = Math.sin(a);
        var edge = (rx * ry) / Math.sqrt(ry * ry * c * c + rx * rx * s * s);
        ray.style.width = h * (0.35 + (i % 2) * 0.2) + 'px';
        ray.parentNode.style.transform = 'rotate(' + a + 'rad)';
        tl.add(ray, {
          x: [edge * 0.8, edge + h * 0.7],
          scaleX: [{ from: 0, to: 1, duration: 160, ease: 'out(2)' }, { to: 0, duration: 380, ease: 'in(2)' }],
          opacity: [{ from: 0, to: 1, duration: 60 }, { to: 1, duration: 300 }, { to: 0, duration: 180 }],
          duration: 540, ease: 'out(3)'
        }, 110);
      });
      o.sparks.forEach(function (sp) {
        var a = Math.random() * Math.PI * 2;
        var dist = 0.9 + Math.random() * 0.7;
        tl.add(sp, {
          x: [Math.cos(a) * rx * 0.4, Math.cos(a) * (rx + h * 0.6) * dist],
          y: [Math.sin(a) * ry * 0.4, Math.sin(a) * h * 1.25 * dist + h * 0.25],
          scale: [1.2, 0.3],
          opacity: [{ from: 0, to: 1, duration: 60 }, { to: 1, duration: 400 }, { to: 0, duration: 500 }],
          duration: 960, ease: 'out(3)'
        }, 140 + Math.random() * 120);
      });
      return tl;
    }

    function run(o) {
      var d = o.el.dataset;
      var loop = d.loop !== undefined && d.loop !== 'false';
      var hold = +d.hold || DEFAULTS.hold;
      clearTimeout(o.timer);
      var cycle = function () {
        sting(o);
        if (!loop) return;
        o.timer = setTimeout(function () {
          anime.animate(o.logo, { opacity: 0, scale: 0.85, duration: 380, ease: 'in(2)' });
          o.timer = setTimeout(cycle, 650);
        }, 1300 + hold);
      };
      anime.utils.set(o.logo, { opacity: 0 });
      o.timer = setTimeout(cycle, +d.delay || 150);
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          run(e.target._lst);
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-logo-sting]').forEach(function (el) {
        if (el._lst) return;
        el._lst = setup(el);
        if (reduced()) return;
        anime.utils.set(el._lst.logo, { opacity: 0 });
        io.observe(el);
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.logoSting = function (el) {
      if (el && el._lst && !reduced()) { clearTimeout(el._lst.timer); sting(el._lst); }
    };

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-logo-sting data-color="#c084fc"
     style="font-size: 56px; font-weight: 800; gap: 0.25em">
  <img src="/mark.svg" alt="" width="56"> animacious
</div>