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
- Paste the snippet just before
</head>. - Wrap the word to point at in
<span data-arrow-scribble>.
Options
Add any of these attributes to the same element.
data-from | left (default) or right: the side the arrow comes from. |
data-note | A short handwritten note at the arrow’s tail. |
data-color | The arrow and note. Default #e11d48. |
data-duration | Time to draw the arrow. Default 900 ms. |
data-loop | Wipe it away and draw it again. |
data-hold | How long it stays, when looping. Default 1800 ms. |
data-delay | Wait 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.
<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><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>