← Text reveals

Underline Draw

A loose, hand-drawn underline draws itself under a word, as if someone just marked it with a pen. Choose a single stroke or a double one.

Words that matter

How to use it

  1. Paste the snippet just before </head>.
  2. Wrap the word or phrase in <span data-underline-draw>.

Options

Add any of these attributes to the same element.

data-shapesingle (default) or double.
data-colorStroke colour. Default #a855f7.
data-loopErase and redraw every few seconds.
data-durationTime to draw. Default 900 ms.
data-holdHow long it stays, when looping. Default 1800 ms.

The stroke scales with the text’s size, so it looks right on a heading or a paragraph.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Underline Draw: a loose, hand-drawn underline draws itself under a word,
    // as if someone just marked it with a pen.
    // Usage: <h1>Make it <span data-underline-draw>count</span></h1>
    // Optional: data-color (default #a855f7), data-shape ("single" or "double"),
    // data-duration (ms), data-loop (erase and redraw), data-hold (ms).
    var DEFAULTS = { color: '#a855f7', duration: 900, hold: 1800 };
    var SHAPES = {
      single: ['M1 6.5 C 18 3.2, 42 8.4, 66 5.2 S 90 3.6, 99 5.4'],
      double: ['M1 5 C 20 2.6, 46 7, 70 4.4 S 92 3.4, 99 4.6', 'M7 9.2 C 30 7.4, 60 10, 93 8']
    };

    var css =
      '.udr{position:relative;display:inline-block;white-space:nowrap}' +
      '.udr-svg{position:absolute;left:-3%;width:106%;top:88%;height:0.32em;overflow:visible;pointer-events:none}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function build(el) {
      var d = el.dataset;
      var paths = SHAPES[d.shape] || SHAPES.single;
      var width = Math.max(2, parseFloat(getComputedStyle(el).fontSize) * 0.075);
      el.classList.add('udr');
      var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
      svg.setAttribute('class', 'udr-svg');
      svg.setAttribute('viewBox', '0 0 100 10');
      svg.setAttribute('preserveAspectRatio', 'none');
      svg.setAttribute('aria-hidden', 'true');
      svg.innerHTML = paths.map(function (p) {
        return '<path d="' + p + '" fill="none" stroke="' + (d.color || DEFAULTS.color) + '" stroke-width="' + width +
          '" stroke-linecap="round" vector-effect="non-scaling-stroke"/>';
      }).join('');
      el.appendChild(svg);
      return anime.svg.createDrawable(svg.querySelectorAll('path'));
    }

    function run(el, strokes) {
      var d = el.dataset;
      var duration = +d.duration || DEFAULTS.duration;
      var loop = d.loop !== undefined && d.loop !== 'false';
      (function drawIn() {
        anime.animate(strokes, {
          draw: ['0 0', '0 1'],
          duration: duration,
          // A second stroke follows the first, like a pen going back over it.
          delay: anime.stagger(duration * 0.55),
          ease: 'inOut(2)',
          onComplete: loop ? function () {
            setTimeout(function () {
              anime.animate(strokes, {
                draw: '1 1', duration: duration * 0.5, ease: 'in(2)',
                onComplete: function () { anime.utils.set(strokes, { draw: '0 0' }); setTimeout(drawIn, 300); }
              });
            }, +d.hold || DEFAULTS.hold);
          } : undefined
        });
      })();
    }

    function init() {
      var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          run(e.target, e.target._udr);
        });
      }, { threshold: 0.6 });
      document.querySelectorAll('[data-underline-draw]').forEach(function (el) {
        if (el._udr) return;
        var strokes = (el._udr = build(el));
        if (reduced) return;
        anime.utils.set(strokes, { draw: '0 0' });
        io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<h1>Make it <span data-underline-draw>count</span></h1>

<!-- a double stroke, in pink: -->
<span data-underline-draw data-shape="double" data-color="#ec4899">matter</span>