← Text effects

Striped Shadow Text

A drop shadow made of fine diagonal stripes sits just behind the text, and the stripes scroll steadily along it, like a retro poster that won’t keep still. Hover or touch the text and the shadow pushes further out.

Old school

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-striped-shadow-text to a heading. Big, heavy type shows the stripes best.

Options

Add any of these attributes to the same element.

data-colorStripe colour. Default #a855f7.
data-depthHow far the shadow sits from the text, in em. Default 0.1.
data-sizeDistance between stripes, in em. Default 0.12.
data-speedHow long the stripes take to move along by one stripe. Default 700 ms; 0 keeps them still.
data-directionright (default) or left.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Striped Shadow Text: a drop shadow made of diagonal stripes sits behind
    // the text and the stripes scroll steadily along it, like a retro poster.
    // Hovering or touching the text pushes the shadow out further.
    // Usage: <h1 data-striped-shadow-text>Retro</h1>
    // Optional: data-color (stripe colour; default #a855f7), data-depth (how far
    // the shadow sits from the text, in em; default 0.1), data-size (distance
    // between stripes, in em; default 0.12), data-speed (ms for the stripes to
    // move along by one stripe; default 700; 0 keeps them still),
    // data-direction (right or left; default right).
    var DEFAULTS = { color: '#a855f7', depth: 0.1, size: 0.12, speed: 700 };

    var css =
      '.sst-wrap{position:relative;display:inline-block;isolation:isolate}' +
      '.sst-shadow{position:absolute;inset:0;z-index:-1;color:transparent;-webkit-text-fill-color:transparent;-webkit-text-stroke:0;' +
      '-webkit-background-clip:text;background-clip:text;pointer-events:none;user-select:none;will-change:transform}';
    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;
      var color = d.color || DEFAULTS.color;
      var depth = d.depth !== undefined ? +d.depth : DEFAULTS.depth;
      var size = +d.size || DEFAULTS.size;
      var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
      var dir = d.direction === 'left' ? -1 : 1;

      // Wrap the content and put a copy of it behind as the shadow.
      var wrap = document.createElement('span');
      wrap.className = 'sst-wrap';
      while (el.firstChild) wrap.appendChild(el.firstChild);
      var shadow = document.createElement('span');
      shadow.className = 'sst-shadow';
      shadow.setAttribute('aria-hidden', 'true');
      shadow.textContent = wrap.textContent;
      shadow.style.backgroundImage = 'linear-gradient(-45deg,' + color + ' 25%,transparent 25%,transparent 50%,' + color + ' 50%,' + color + ' 75%,transparent 75%)';
      shadow.style.backgroundSize = size + 'em ' + size + 'em';
      wrap.insertBefore(shadow, wrap.firstChild);
      el.appendChild(wrap);

      var push = { depth: depth };
      function place() { shadow.style.transform = 'translate(' + push.depth + 'em,' + push.depth + 'em)'; }
      place();
      if (reduced()) return;

      // Scroll the stripes: one tile per cycle loops seamlessly.
      var timer = null;
      if (speed > 0) {
        timer = anime.createTimer({
          duration: speed,
          loop: true,
          autoplay: false,
          onUpdate: function (t) {
            var p = (t.currentTime % speed) / speed;
            shadow.style.backgroundPosition = (dir * p * size).toFixed(4) + 'em 0';
          }
        });
        new IntersectionObserver(function (entries) {
          if (entries[0].isIntersecting) timer.play(); else timer.pause();
        }).observe(el);
      }

      // Hover or touch pushes the shadow out, letting go settles it back.
      function out(to) {
        anime.animate(push, { depth: to, duration: to > depth ? 380 : 700, ease: to > depth ? 'outBack(2)' : 'outElastic(1, .5)', onUpdate: place });
      }
      el.addEventListener('pointerenter', function () { out(depth * 1.9 + 0.02); });
      el.addEventListener('pointerleave', function () { out(depth); });
      el.addEventListener('pointercancel', function () { out(depth); });
      el.addEventListener('pointerup', function (e) { if (e.pointerType !== 'mouse') out(depth); });
      el.addEventListener('pointerdown', function (e) { if (e.pointerType !== 'mouse') out(depth * 1.9 + 0.02); });
    }

    function init() {
      document.querySelectorAll('[data-striped-shadow-text]').forEach(function (el) {
        if (el._sst) return;
        el._sst = true;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<h1 data-striped-shadow-text>Old school</h1>

<!-- a deeper orange shadow, scrolling slowly to the left: -->
<h1 data-striped-shadow-text data-color="#f97316" data-depth="0.16" data-speed="1400" data-direction="left">Old school</h1>