← Entrances

Back In

The element glides in from off screen at reduced size, as if it’s still far away, then steps forward to full size once it’s in place.

ReminderTeam call in 10 minutes

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-back-in to any element.
  3. Choose an edge with data-from if it shouldn’t come from the top.

Options

Add any of these attributes to the same element.

data-fromThe edge it comes from: top, bottom, left or right. Give several and each loop uses the next. Default top.
data-scaleIts size on the way in, before it grows. Default 0.7.
data-distanceTravel this many px instead of starting fully off screen.
data-durationHow long the whole entrance takes. Default 1200 ms.
data-delayWait before the first entrance. Default 0 ms.
data-loopHold, fade away and come in again.
data-holdHow long it stays before fading, when looping. Default 1400 ms.

It starts just past the edge of the nearest container that clips its overflow (overflow: hidden or clip), or of the window if there isn’t one. Put it in a clipping wrapper, or give the page overflow-x: clip, so it can’t make the page scroll sideways while it’s off screen.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Back In: the element approaches from off screen at reduced size, as if
    // from a distance, then grows to full size once it's in place.
    // Usage: <div data-back-in>…</div>
    // It starts just past the edge of the nearest container that clips its
    // overflow (overflow: hidden or clip), or of the window.
    // Optional: data-from ("top", default, "bottom", "left" or "right"; give
    // several, like "top right left", and each loop uses the next),
    // data-distance (px to travel instead of from off screen), data-scale (its
    // size on the way in, default 0.7), data-duration (ms, default 1200),
    // data-delay (ms before the first entrance), data-loop (hold, fade away
    // and come in again), data-hold (ms to stay when looping, default 1400).
    var DEFAULTS = { from: 'top', scale: 0.7, duration: 1200, delay: 0, 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 };
    }

    // Where to start: an x/y offset that puts the element just outside the box.
    function offset(el, from) {
      var d = +el.dataset.distance;
      var r = el.getBoundingClientRect();
      var c = clipBox(el);
      if (from === 'right') return { x: d || c.right - r.left + 8, y: 0 };
      if (from === 'left') return { x: -(d || r.right - c.left + 8), y: 0 };
      if (from === 'bottom') return { x: 0, y: d || c.bottom - r.top + 8 };
      return { x: 0, y: -(d || r.bottom - c.top + 8) };
    }

    function enter(el, from, done) {
      var d = el.dataset;
      var t = +d.duration || DEFAULTS.duration;
      var o = offset(el, from);
      anime.utils.set(el, { x: o.x, y: o.y, scale: +d.scale || DEFAULTS.scale, opacity: 0.7 });
      anime.createTimeline({ onComplete: done })
        // Glide in while still small and far away...
        .add(el, { x: 0, y: 0, opacity: 1, duration: t * 0.6, ease: 'out(3)' })
        // ...then step forward to full size.
        .add(el, { scale: 1, duration: t * 0.4, ease: 'outBack(1.3)' });
    }

    function cycle(el) {
      var d = el.dataset;
      var s = el._bki;
      var froms = (d.from || DEFAULTS.from).split(/[\s,]+/);
      enter(el, froms[s.n++ % froms.length], function () {
        if (d.loop === undefined || d.loop === 'false') return;
        setTimeout(function () {
          anime.animate(el, {
            opacity: 0,
            duration: 400,
            ease: 'out(2)',
            onComplete: function () {
              // Wait until it's back on screen before the next entrance.
              setTimeout(function () {
                if (s.visible) cycle(el);
                else s.waiting = true;
              }, 500);
            }
          });
        }, +d.hold || DEFAULTS.hold);
      });
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          var s = e.target._bki;
          s.visible = e.isIntersecting;
          if (!e.isIntersecting) return;
          if (!s.started) {
            s.started = true;
            setTimeout(function () { cycle(e.target); }, +e.target.dataset.delay || DEFAULTS.delay);
          } else if (s.waiting) {
            s.waiting = false;
            cycle(e.target);
          }
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-back-in]').forEach(function (el) {
        if (el._bki) return;
        el._bki = { n: 0 };
        if (reduced()) return;
        anime.utils.set(el, { opacity: 0 });
        io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div class="card" data-back-in>Reminder</div>

<!-- or from the left: -->
<div class="card" data-back-in data-from="left">Reminder</div>