← SVG & icons

Logo Split Reveal

A line of light runs down the middle of the logo, then the two halves glide apart to reveal a tagline between them, its letters closing up as it fades in. Where there isn’t room side by side, as on a phone, it splits top and bottom instead.

animacious

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-split-reveal.
  3. Put the tagline in data-tagline. It’s 0.36 of the logo’s font size; style .lsr-tag-in to change it.

Options

Add any of these attributes to the same element.

data-directionauto (side by side when there’s room, else top and bottom), horizontal or vertical. Default auto.
data-durationTime for the halves to part. Default 900 ms.
data-loopPart, hold, close up and repeat.
data-holdHow long it stays open, when looping. Default 2200 ms.
data-colorColour of the line of light. Default the text colour.
data-delayWait before it starts.

It starts when scrolled into view. The halves are two copies of your logo, the second hidden from screen readers, so give the wrapper a role="img" and an aria-label that includes the tagline. With reduced motion it simply shows open.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Logo Split Reveal: a line of light runs down the middle of the logo,
    // then the two halves slide apart to reveal a tagline between them. Loop
    // it and they close up again. Where there isn't room side by side (a
    // phone, say) the logo splits top and bottom instead.
    // Usage: <div data-logo-split-reveal data-tagline="Animation made simple">
    //          <svg …>…</svg> Brand
    //        </div>
    // Optional: data-direction ("auto" (default), "horizontal" or "vertical"),
    // data-duration (ms for the halves to part, default 900), data-delay (ms
    // before it starts), data-loop (part, hold, close and repeat), data-hold
    // (ms it stays open when looping, default 2200), data-color (the line of
    // light, default the text colour). Style the tagline with .lsr-tag.
    var DEFAULTS = { duration: 900, hold: 2200 };

    var css =
      '.lsr{position:relative;display:inline-grid;vertical-align:middle}' +
      '.lsr-half{grid-area:1/1;display:flex;align-items:center;justify-content:center;gap:inherit}' +
      '.lsr-tag{position:absolute;left:50%;top:50%;width:max-content;transform:translate(-50%,-50%);text-align:center;pointer-events:none}' +
      '.lsr-tag-in{display:block;font-size:.36em;font-weight:600;letter-spacing:.02em;line-height:1.25;opacity:0}' +
      '.lsr-seam{position:absolute;left:50%;top:-8%;bottom:-8%;width:2px;margin-left:-1px;border-radius:2px;opacity:0;pointer-events:none;' +
      'background:linear-gradient(transparent,currentColor 30%,currentColor 70%,transparent);box-shadow:0 0 12px currentColor}' +
      '.lsr-v .lsr-seam{left:-6%;right:-6%;top:50%;bottom:auto;width:auto;height:2px;margin:-1px 0 0;background:linear-gradient(90deg,transparent,currentColor 30%,currentColor 70%,transparent)}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

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

    function setup(el) {
      var d = el.dataset;
      el.classList.add('lsr');
      // Two copies of the logo, each clipped to one half.
      var a = document.createElement('div');
      a.className = 'lsr-half';
      while (el.firstChild) a.appendChild(el.firstChild);
      var b = a.cloneNode(true);
      b.setAttribute('aria-hidden', 'true');
      // Ids in the copy would clash with the original's.
      Array.prototype.forEach.call(b.querySelectorAll('[id]'), function (n) { n.removeAttribute('id'); });
      var tag = document.createElement('div');
      tag.className = 'lsr-tag';
      tag.innerHTML = '<span class="lsr-tag-in"></span>';
      tag.firstChild.textContent = d.tagline || '';
      var seam = document.createElement('div');
      seam.className = 'lsr-seam';
      seam.setAttribute('aria-hidden', 'true');
      if (d.color) seam.style.color = d.color;
      el.appendChild(a);
      el.appendChild(b);
      el.appendChild(tag);
      el.appendChild(seam);
      var s = { el: el, a: a, b: b, tag: tag, text: tag.firstChild, seam: seam, open: false, dir: d.direction || 'auto' };
      el._lsr = s;
      measure(s);
      if ('ResizeObserver' in window) {
        var last = 0;
        new ResizeObserver(function () {
          var w = (el.parentElement || document.body).clientWidth;
          if (w === last) return;
          last = w;
          measure(s);
          place(s);
        }).observe(el.parentElement || document.body);
      }
      return s;
    }

    // Works out the direction, how far the halves part and how the tagline wraps.
    function measure(s) {
      var parent = s.el.parentElement || document.body;
      var cs = getComputedStyle(parent);
      var avail = parent.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight);
      var text = s.text;
      s.tag.style.maxWidth = '';
      s.tag.style.whiteSpace = 'nowrap';
      var w = s.a.offsetWidth;
      var tw = s.tag.offsetWidth;
      var pad = parseFloat(getComputedStyle(text).fontSize) * 0.9;
      var dir = s.dir === 'vertical' || s.dir === 'horizontal' ? s.dir : (w + tw + pad * 2 <= avail ? 'horizontal' : 'vertical');
      var room = dir === 'horizontal' ? avail - w - pad * 2 : avail;
      if (tw > room) {
        s.tag.style.whiteSpace = 'normal';
        s.tag.style.maxWidth = Math.max(60, room) + 'px';
      }
      s.vertical = dir === 'vertical';
      s.el.classList.toggle('lsr-v', s.vertical);
      s.gap = s.vertical ? s.tag.offsetHeight + pad * 2 : s.tag.offsetWidth + pad * 2;
      s.a.style.clipPath = s.vertical ? 'inset(-30% -30% 50% -30%)' : 'inset(-30% 50% -30% -30%)';
      s.b.style.clipPath = s.vertical ? 'inset(50% -30% -30% -30%)' : 'inset(-30% -30% -30% 50%)';
    }

    function offsets(s, open) {
      var g = open ? s.gap / 2 : 0;
      return s.vertical ? [{ x: 0, y: -g }, { x: 0, y: g }] : [{ x: -g, y: 0 }, { x: g, y: 0 }];
    }

    function place(s) {
      var o = offsets(s, s.open);
      anime.utils.set(s.a, o[0]);
      anime.utils.set(s.b, o[1]);
      anime.utils.set(s.text, { opacity: s.open ? 1 : 0 });
    }

    function open(s, duration) {
      s.open = true;
      var o = offsets(s, true);
      var tl = anime.createTimeline();
      tl.add(s.seam, { opacity: [0, 1], scaleY: s.vertical ? 1 : [0, 1], scaleX: s.vertical ? [0, 1] : 1, duration: 380, ease: 'out(3)' })
        .add(s.a, { x: o[0].x, y: o[0].y, duration: duration, ease: 'inOut(4)' }, '+=80')
        .add(s.b, { x: o[1].x, y: o[1].y, duration: duration, ease: 'inOut(4)' }, '<<')
        .add(s.seam, { opacity: 0, duration: duration * 0.6, ease: 'out(2)' }, '<<')
        .add(s.text, { opacity: [0, 1], letterSpacing: ['0.4em', '0.02em'], scale: [0.92, 1], duration: duration, ease: 'out(3)' }, '-=' + duration * 0.55);
    }

    function close(s) {
      s.open = false;
      var o = offsets(s, false);
      var tl = anime.createTimeline();
      tl.add(s.text, { opacity: 0, scale: 0.94, duration: 320, ease: 'in(2)' })
        .add(s.a, { x: o[0].x, y: o[0].y, duration: 650, ease: 'inOut(3)' }, '-=120')
        .add(s.b, { x: o[1].x, y: o[1].y, duration: 650, ease: 'inOut(3)' }, '<<')
        // A small bump as the halves meet.
        .add(s.el, { scale: [1, 1.03, 1], duration: 380, ease: 'out(2)' });
    }

    function play(s) {
      var d = s.el.dataset;
      var duration = +d.duration || DEFAULTS.duration;
      var hold = +d.hold || DEFAULTS.hold;
      var loop = d.loop !== undefined && d.loop !== 'false';
      var cycle = function () {
        measure(s);
        open(s, duration);
        if (loop) setTimeout(function () {
          close(s);
          setTimeout(cycle, 1600);
        }, duration + 500 + hold);
      };
      setTimeout(cycle, +d.delay || 300);
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          play(e.target._lsr);
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-logo-split-reveal]').forEach(function (el) {
        if (el._lsr) return;
        var s = setup(el);
        if (reduced()) { s.open = true; place(s); }
        else io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-logo-split-reveal data-tagline="Animation, made simple"
     role="img" aria-label="Animacious: Animation, made simple"
     style="font-size: 56px; font-weight: 800; gap: 0.25em">
  <img src="/mark.svg" alt="" width="56"> animacious
</div>