← Text reveals

Strikethrough Draw

A quick pen line strikes through a word, the word fades back, and a replacement writes itself in beside it, letter by letter. Handy for changed prices, dates and second thoughts.

Ship it next week

How to use it

  1. Paste the snippet just before </head>.
  2. Wrap the word to cross out in <span data-strikethrough-draw>.
  3. Put its replacement in data-replace, or leave it out to strike the word only.

Options

Add any of these attributes to the same element.

data-replaceThe word that writes itself in beside the struck one.
data-colorThe strike line. Default #e11d48.
data-new-colorThe replacement word. Default #a855f7.
data-durationTime to draw the line. Default 550 ms.
data-loopTake it all back and strike again, with a slightly different line each time.
data-holdHow long it stays, when looping. Default 1800 ms.
data-delayWait before starting. Default 0 ms.

Screen readers hear the old word followed by “now” and the new one.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Strikethrough Draw: a hand-drawn line strikes through a word, the word
    // fades back, and a replacement writes itself in beside it.
    // Usage: <p>Ship it <span data-strikethrough-draw data-replace="today">next week</span></p>
    // Optional: data-replace (the new word; leave it out for a strike only),
    // data-color (the line, default "#e11d48"), data-new-color (the new word,
    // default "#a855f7"), data-duration (ms to draw the line, default 550),
    // data-loop (take it all back and strike again), data-hold (ms to stay,
    // when looping, default 1800), data-delay (ms).
    var DEFAULTS = { color: '#e11d48', newColor: '#a855f7', duration: 550, hold: 1800, delay: 0 };
    // Drawn in a 100 x 20 box stretched over the word: each one a quick,
    // slightly rising pen stroke that runs a little past both ends.
    var STROKES = [
      'M1 13 C 20 12, 42 12.6, 62 10.6 C 78 9.2, 90 9.4, 99 7.6',
      'M1.5 11.5 C 24 13, 46 10.4, 66 11 C 80 11.4, 91 9, 98.5 9.6',
      'M1 14 C 18 11.6, 40 11.8, 58 11 C 76 10.2, 88 8.4, 99 8.8'
    ];

    var css =
      '.sthd{white-space:normal}' +
      '.sthd-old{position:relative;display:inline-block;white-space:nowrap}' +
      '.sthd-svg{position:absolute;left:-5%;width:110%;top:32%;height:40%;overflow:visible;pointer-events:none}' +
      '.sthd-new{display:inline-block;white-space:nowrap;vertical-align:bottom;overflow:hidden;padding:0 .06em .14em;margin-bottom:-.14em}' +
      '.sthd-ch{display:inline-block;will-change:transform}' +
      '.sthd-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}';
    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 text = el.textContent.replace(/\s+/g, ' ').trim();
      var replace = (d.replace || '').trim();
      el.classList.add('sthd');
      el.textContent = '';

      var old = document.createElement('span');
      old.className = 'sthd-old';
      var txt = document.createElement('span');
      txt.textContent = text;
      old.appendChild(txt);
      var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
      svg.setAttribute('class', 'sthd-svg');
      svg.setAttribute('aria-hidden', 'true');
      var width = Math.max(2, parseFloat(getComputedStyle(el).fontSize) * 0.075);
      svg.innerHTML = '<path d="' + STROKES[0] + '" fill="none" stroke="' + (d.color || DEFAULTS.color) +
        '" stroke-width="' + width.toFixed(1) + '" stroke-linecap="round"/>';
      old.appendChild(svg);
      el.appendChild(old);

      var s = { old: old, txt: txt, svg: svg, path: svg.querySelector('path'), chars: [], n: 0 };
      if (replace) {
        // Screen readers hear "next week, now today"; the animated letters are hidden from them.
        var sr = document.createElement('span');
        sr.className = 'sthd-sr';
        sr.textContent = ', now ' + replace;
        var neu = document.createElement('span');
        neu.className = 'sthd-new';
        neu.setAttribute('aria-hidden', 'true');
        neu.style.color = d.newColor || DEFAULTS.newColor;
        Array.from(replace).forEach(function (c) {
          var ch = document.createElement('span');
          ch.className = 'sthd-ch';
          ch.textContent = c === ' ' ? ' ' : c;
          neu.appendChild(ch);
          s.chars.push(ch);
        });
        el.appendChild(document.createTextNode(' '));
        el.appendChild(neu);
        el.appendChild(sr);
        s.neu = neu;
      }
      return s;
    }

    // Scale a stroke from its 100 x 20 box to the word's real size in pixels,
    // so the line keeps an even thickness and draws its full length.
    function fit(s) {
      var r = s.svg.getBoundingClientRect(), i = 0;
      var w = Math.max(1, r.width), h = Math.max(1, r.height);
      s.svg.setAttribute('viewBox', '0 0 ' + w.toFixed(1) + ' ' + h.toFixed(1));
      s.path.setAttribute('d', STROKES[s.n++ % STROKES.length].replace(/-?\d*\.?\d+/g, function (n) {
        return (+n * (i++ % 2 ? h / 20 : w / 100)).toFixed(1);
      }));
      s.line = anime.svg.createDrawable(s.path);
    }

    function play(el) {
      var s = el._sthd, d = el.dataset;
      var loop = d.loop !== undefined && d.loop !== 'false';
      var duration = +d.duration || DEFAULTS.duration;
      // A slightly different stroke each time round, like a real pen.
      fit(s);
      anime.utils.set(s.line, { draw: '0 0' });
      var tl = anime.createTimeline({
        onComplete: function () { if (loop) setTimeout(function () { play(el); }, 350); }
      });
      tl.add(s.line, { draw: ['0 0', '0 1'], duration: duration, ease: 'inOut(1.8)' }, 0);
      tl.add(s.txt, { opacity: [1, 0.42], duration: 400, ease: 'outQuad' }, duration * 0.7);
      var end = duration + 200;
      if (s.neu) {
        s.neu.style.width = 'auto';
        var w = s.neu.offsetWidth;
        s.neu.style.width = '0px';
        tl.add(s.neu, { width: [0, w], duration: 380, ease: 'outQuart' }, duration * 0.85);
        tl.add(s.chars, {
          y: ['0.6em', 0],
          rotate: function () { return [anime.utils.random(-14, 14), 0]; },
          opacity: [0, 1],
          duration: 420,
          delay: anime.stagger(45),
          ease: 'outBack(1.8)'
        }, duration * 0.85 + 60);
        end = duration * 0.85 + 60 + 420 + 45 * s.chars.length;
      }
      if (!loop) {
        // Let the new word size itself again, so it follows any resize.
        if (s.neu) tl.call(function () { s.neu.style.width = 'auto'; }, end);
        return;
      }
      var out = end + (+d.hold || DEFAULTS.hold);
      if (s.neu) {
        tl.add(s.chars, { opacity: 0, y: '-0.3em', duration: 260, delay: anime.stagger(25), ease: 'inQuad' }, out);
        tl.add(s.neu, { width: 0, duration: 320, ease: 'inOutQuad' }, out + 200);
      }
      tl.add(s.line, { draw: '1 1', duration: duration * 0.7, ease: 'inOut(2)' }, out + 250);
      tl.add(s.txt, { opacity: 1, duration: 350, ease: 'outQuad' }, out + 350);
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          var el = e.target;
          setTimeout(function () { play(el); }, +el.dataset.delay || DEFAULTS.delay);
        });
      }, { threshold: 0.5 });
      document.querySelectorAll('[data-strikethrough-draw]').forEach(function (el) {
        if (el._sthd) return;
        var s = el._sthd = setup(el);
        fit(s);
        if (reduced()) {
          s.txt.style.opacity = 0.42;
          return;
        }
        anime.utils.set(s.line, { draw: '0 0' });
        if (s.neu) {
          s.neu.style.width = '0px';
          s.chars.forEach(function (c) { c.style.opacity = 0; });
        }
        io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<p>Ship it <span data-strikethrough-draw data-replace="today">next week</span></p>

<!-- or, a strike only, in black: -->
<p>Was <span data-strikethrough-draw data-color="#111">$49</span> $29</p>