← Entrances

Light Speed Out

The element crouches back for a moment, then shoots off the side at speed, its top trailing behind as it leans into the dash.

Message sentDelivered to Priya

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-light-speed-out to any element.

Options

Add any of these attributes to the same element.

data-toThe side it leaves by: right or left. Give both, right left, and each loop uses the next. Default right.
data-skewHow far it leans at full speed, in degrees. Default 30.
data-distanceTravel this many px instead of leaving the screen.
data-durationHow long it takes to leave. Default 650 ms.
data-delayHow long it stays in view before it leaves. Default 1200 ms.
data-loopFade back in, hold and shoot off again.
data-holdHow long it stays between exits, when looping. Default 1400 ms.

It leaves past the edge of the nearest container that clips its overflow (overflow: hidden or clip), or of the window. Put it in a clipping wrapper, or give the page overflow-x: clip, so it can’t make the page scroll sideways on its way out. It ends hidden (visibility: hidden) but still taking up its space. To play it when you choose, leave the attribute off and call animacious.lightSpeedOut(el).

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Light Speed Out: the element crouches back for a moment, then shoots
    // off the side at speed, leaning back as it goes and fading as it leaves.
    // Usage: <div data-light-speed-out>…</div>
    // It plays once the element has been in view for data-delay ms. It leaves
    // past the edge of the nearest container that clips its overflow
    // (overflow: hidden or clip), or of the window, and ends hidden
    // (visibility: hidden) but still taking up its space.
    // Optional: data-to ("right", default, or "left"; give both, "right
    // left", and each loop uses the next), data-skew (degrees of lean at full
    // speed, default 30), data-distance (px to travel instead of leaving the
    // screen),
    // data-duration (ms, default 650), data-delay (ms in view before it
    // leaves, default 1200), data-loop (fade back in, hold and shoot off
    // again), data-hold (ms to stay between exits when looping, default 1400).
    // Script use: animacious.lightSpeedOut(el, done) plays it on any element now,
    // using its data-* options, and calls done afterwards.
    var DEFAULTS = { to: 'right', skew: 30, duration: 650, delay: 1200, hold: 1400 };

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

    // The box the element must leave to be "off screen".
    function clipBox(el) {
      for (var p = el.parentElement; p && p !== document.body; p = p.parentElement) {
        var s = getComputedStyle(p);
        if (s.overflowX !== 'visible' || s.overflowY !== 'visible') return p.getBoundingClientRect();
      }
      return { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight };
    }

    function leave(el, to, done) {
      var d = el.dataset;
      var t = +d.duration || DEFAULTS.duration;
      if (reduced()) {
        anime.utils.set(el, { opacity: 0 });
        if (done) done();
        return;
      }
      var skew = d.skew === undefined ? DEFAULTS.skew : +d.skew || 0;
      anime.utils.set(el, { x: 0, skewX: 0 });
      var r = el.getBoundingClientRect();
      var c = clipBox(el);
      // +1 when it leaves to the right, -1 to the left.
      var dir = to === 'left' ? -1 : 1;
      // A skewed box reaches further sideways, so allow for its lean.
      var lean = Math.tan(skew * Math.PI / 180) * r.height / 2;
      var dist = +d.distance || (dir > 0 ? c.right - r.left : r.right - c.left) + lean + 8;
      anime.animate(el, {
        // A short wind-up, then away, its top trailing behind.
        x: [
          { to: -dir * Math.min(14, r.width * 0.05), duration: t * 0.25, ease: 'out(2)' },
          { to: dir * dist, duration: t * 0.75, ease: 'in(3)' }
        ],
        skewX: [
          { to: -dir * skew * 0.25, duration: t * 0.25, ease: 'out(2)' },
          { to: dir * skew, duration: t * 0.45, ease: 'in(2)' }
        ],
        opacity: { to: 0, delay: t * 0.5, duration: t * 0.5, ease: 'in(2)' },
        onComplete: function () {
          if (done) done();
        }
      });
    }

    function cycle(el) {
      var d = el.dataset;
      var s = el._lso;
      var list = (d.to || DEFAULTS.to).split(/[\s,]+/);
      leave(el, list[s.n++ % list.length], function () {
        if (d.loop === undefined || d.loop === 'false') {
          el.style.visibility = 'hidden';
          return;
        }
        // Come back quietly in place, then go again once it's on screen.
        setTimeout(function () {
          anime.utils.set(el, { x: 0, skewX: 0, opacity: 0 });
          anime.animate(el, {
            opacity: 1,
            duration: 400,
            ease: 'out(2)',
            onComplete: function () {
              setTimeout(function () {
                if (s.visible) cycle(el);
                else s.waiting = true;
              }, +d.hold || DEFAULTS.hold);
            }
          });
        }, 400);
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.lightSpeedOut = function (el, done) {
      leave(el, (el.dataset.to || DEFAULTS.to).split(/[\s,]+/)[0], function () {
        el.style.visibility = 'hidden';
        if (done) done(el);
      });
    };

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          var s = e.target._lso;
          s.visible = e.isIntersecting;
          if (!e.isIntersecting) return;
          if (!s.started) {
            s.started = true;
            var wait = e.target.dataset.delay;
            setTimeout(function () { cycle(e.target); }, wait === undefined ? DEFAULTS.delay : +wait || 0);
          } else if (s.waiting) {
            s.waiting = false;
            cycle(e.target);
          }
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-light-speed-out]').forEach(function (el) {
        if (el._lso) return;
        el._lso = { n: 0 };
        if (!reduced()) io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div class="card" data-light-speed-out>Message sent</div>

<!-- or to the left, leaning harder: -->
<div class="card" data-light-speed-out data-to="left" data-skew="40">Message sent</div>