← Text reveals

Arrow Scribble

A hand-drawn arrow sketches itself from below the line, curls once and swoops up to point at a word, then flicks its head on. Add a scribbled note at its tail if you like.

Try the new look

How to use it

  1. Paste the snippet just before </head>.
  2. Wrap the word to point at in <span data-arrow-scribble>.

Options

Add any of these attributes to the same element.

data-fromleft (default) or right: the side the arrow comes from.
data-noteA short handwritten note at the arrow’s tail.
data-colorThe arrow and note. Default #e11d48.
data-durationTime to draw the arrow. Default 900 ms.
data-loopWipe it away and draw it again.
data-holdHow long it stays, when looping. Default 1800 ms.
data-delayWait before starting. Default 0 ms.

The arrow hangs below the word and scales with the text, so leave a line or so of space under it.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Arrow Scribble: a hand-drawn arrow sketches itself from below the line
    // and points at a word, with an optional scribbled note at its tail.
    // Usage: <p>Try the <span data-arrow-scribble>new</span> look</p>
    // Optional: data-from ("left", default, or "right": which side the arrow
    // comes from), data-note (a short handwritten note at the arrow's tail),
    // data-color (default "#e11d48"), data-duration (ms to draw the arrow,
    // default 900), data-loop (wipe it and draw again), data-hold (ms to stay,
    // when looping, default 1800), data-delay (ms).
    var DEFAULTS = { color: '#e11d48', duration: 900, hold: 1800, delay: 0 };
    // In a 100 x 64 box below and beside the word: the shaft starts at the
    // bottom left, curls once and swoops up to the top right, then the head.
    var SHAFT = 'M6 60 C 12 48, 22 38, 34 40 C 46 42, 44 54, 36 52 C 26 50, 40 28, 60 20 C 72 15, 82 12, 92 10';
    var HEAD = 'M78 3.5 Q 85.5 7 92.6 10 Q 87.5 15.5 83.5 22';

    var css =
      '.arsc{position:relative;display:inline-block;white-space:nowrap}' +
      '.arsc-svg{position:absolute;top:88%;width:2.1em;height:1.35em;overflow:visible;pointer-events:none}' +
      '.arsc-left .arsc-svg{right:58%}' +
      '.arsc-right .arsc-svg{left:58%}' +
      '.arsc-note{font-family:"Segoe Print","Bradley Hand","Chalkboard SE","Comic Sans MS",cursive;font-weight:700;letter-spacing:0}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

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

    function build(el) {
      var d = el.dataset;
      var color = d.color || DEFAULTS.color;
      var right = d.from === 'right';
      el.classList.add('arsc', right ? 'arsc-right' : 'arsc-left');
      var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
      svg.setAttribute('class', 'arsc-svg');
      svg.setAttribute('viewBox', '0 0 100 64');
      svg.setAttribute('aria-hidden', 'true');
      var line = '" fill="none" stroke="' + color + '" stroke-width="4.2" stroke-linecap="round" stroke-linejoin="round"/>';
      // From the right, the same arrow is mirrored inside the drawing.
      svg.innerHTML = '<g' + (right ? ' transform="translate(100 0) scale(-1 1)"' : '') + '>' +
        '<path d="' + SHAFT + line + '<path d="' + HEAD + line + '</g>';
      var note = null;
      if (d.note) {
        // The note sits just past the arrow's tail.
        note = document.createElementNS('http://www.w3.org/2000/svg', 'text');
        note.setAttribute('class', 'arsc-note');
        note.setAttribute('x', right ? '100' : '0');
        note.setAttribute('y', '74');
        note.setAttribute('text-anchor', right ? 'start' : 'end');
        note.setAttribute('font-size', '26');
        note.setAttribute('fill', color);
        note.textContent = d.note;
        svg.appendChild(note);
      }
      el.appendChild(svg);
      var paths = svg.querySelectorAll('path');
      return {
        shaft: anime.svg.createDrawable(paths[0]),
        head: anime.svg.createDrawable(paths[1]),
        note: note
      };
    }

    function play(el) {
      var s = el._arsc, d = el.dataset;
      var loop = d.loop !== undefined && d.loop !== 'false';
      var duration = +d.duration || DEFAULTS.duration;
      anime.utils.set([s.shaft, s.head], { draw: '0 0' });
      var tl = anime.createTimeline({
        onComplete: function () { if (loop) setTimeout(function () { play(el); }, 350); }
      });
      var t = 0;
      if (s.note) {
        // The note is scribbled first, then the arrow leaves from it.
        tl.add(s.note, { opacity: [0, 1], translateY: [6, 0], duration: 380, ease: 'outQuad' }, 0);
        t = 300;
      }
      tl.add(s.shaft, { draw: ['0 0', '0 1'], duration: duration, ease: 'inOut(1.5)' }, t);
      tl.add(s.head, { draw: ['0 0', '0 1'], duration: duration * 0.3, ease: 'out(2)' }, t + duration + 60);
      // The finished arrow gives a tiny nudge towards the word.
      tl.add(el.querySelector('.arsc-svg'), { translateX: [0, d.from === 'right' ? -3 : 3, 0], translateY: [0, -3, 0], duration: 360, ease: 'inOut(2)' }, t + duration * 1.3 + 60);
      if (loop) {
        var out = t + duration * 1.3 + 420 + (+d.hold || DEFAULTS.hold);
        tl.add([s.shaft, s.head], { draw: '1 1', duration: 450, ease: 'inOut(2)' }, out);
        if (s.note) tl.add(s.note, { opacity: 0, duration: 300, ease: 'inQuad' }, out + 150);
      }
    }

    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-arrow-scribble]').forEach(function (el) {
        if (el._arsc) return;
        var s = el._arsc = build(el);
        if (reduced()) return;
        anime.utils.set([s.shaft, s.head], { draw: '0 0' });
        if (s.note) s.note.style.opacity = 0;
        io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<p>Try the <span data-arrow-scribble>new</span> look</p>

<!-- or, from the right, with a note: -->
<p>Try the <span data-arrow-scribble data-from="right" data-note="so good!">new</span> look</p>