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
- Paste the snippet just before
</head>. - Wrap the word or phrase in
<span data-underline-draw>.
Options
Add any of these attributes to the same element.
data-shape | single (default) or double. |
data-color | Stroke colour. Default #a855f7. |
data-loop | Erase and redraw every few seconds. |
data-duration | Time to draw. Default 900 ms. |
data-hold | How 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.
<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><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>