← Text reveals

Mask Wipe Reveal

A clean, hard edge wipes across the text to uncover it, easing in and out like a slide transition. Wipe left, right, up or down.

A clean reveal

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-mask-wipe-reveal to any heading, text element or image.

Options

Add any of these attributes to the same element.

data-directionWhich way the edge travels: right (default), left, up or down.
data-loopReveal, hold, carry on across to wipe it away, and reveal again.
data-holdHow long the text stays before wiping away, when looping. Default 1800 ms.
data-durationTime for each wipe. Default 1100 ms.
data-delayWait before starting. Default 0 ms.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Mask Wipe Reveal: a clean, hard edge wipes across the text to uncover it,
    // easing in and out like a slide transition.
    // Usage: <h1 data-mask-wipe-reveal>A clean reveal</h1>
    // Optional: data-direction (which way the edge travels: "right", the
    // default, "left", "up" or "down"), data-loop (the edge carries on across to
    // wipe the text away, then reveals it again), data-hold (ms to stay before
    // wiping away), data-duration (ms per wipe), data-delay (ms before starting).
    var DEFAULTS = { direction: 'right', duration: 1100, delay: 0, hold: 1800 };

    // Which inset (top, right, bottom, left) hides the text before the wipe, and
    // which one hides it after the edge has carried on through.
    var SIDES = {
      right: { start: 1, end: 3 },
      left: { start: 3, end: 1 },
      up: { start: 0, end: 2 },
      down: { start: 2, end: 0 }
    };

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

    // An inset() clip in px. The mask reaches a little past the box so
    // descenders and italics aren't trimmed; `side` is pulled all the way across.
    function inset(el, side) {
      var pad = parseFloat(getComputedStyle(el).fontSize) * 0.25;
      var v = [-pad, -pad, -pad, -pad];
      if (side !== undefined) v[side] = (side % 2 ? el.offsetWidth : el.offsetHeight) + pad;
      return 'inset(' + v.join('px ') + 'px)';
    }

    function play(el) {
      var d = el.dataset;
      var loop = d.loop !== undefined && d.loop !== 'false';
      var duration = +d.duration || DEFAULTS.duration;
      var sides = SIDES[d.direction] || SIDES[DEFAULTS.direction];

      var tl = anime.createTimeline({
        loop: loop,
        loopDelay: 400,
        delay: +d.delay || DEFAULTS.delay,
        onComplete: function () { el.style.clipPath = ''; }
      });
      el.style.clipPath = inset(el, sides.start);
      el.style.opacity = '';
      tl.add(el, {
        clipPath: [inset(el, sides.start), inset(el)],
        duration: duration,
        ease: 'inOutQuart'
      });
      if (loop) {
        tl.add(el, {
          clipPath: [inset(el), inset(el, sides.end)],
          duration: duration * 0.8,
          ease: 'inOutQuart'
        }, '+=' + (+d.hold || DEFAULTS.hold));
      }
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          play(e.target);
        });
      }, { threshold: 0.3 });
      document.querySelectorAll('[data-mask-wipe-reveal]').forEach(function (el) {
        if (el._mwr) return;
        el._mwr = true;
        if (reduced()) return;
        // Hidden with opacity, not a clip, until it plays: the browser won't
        // report a fully clipped element as scrolled into view.
        el.style.opacity = 0;
        io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<h1 data-mask-wipe-reveal>A clean reveal</h1>

<!-- wiping upwards, looping: -->
<h1 data-mask-wipe-reveal data-direction="up" data-loop>A clean reveal</h1>