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
- Paste the snippet just before
</head>. - Wrap the word to cross out in
<span data-strikethrough-draw>. - 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-replace | The word that writes itself in beside the struck one. |
data-color | The strike line. Default #e11d48. |
data-new-color | The replacement word. Default #a855f7. |
data-duration | Time to draw the line. Default 550 ms. |
data-loop | Take it all back and strike again, with a slightly different line each time. |
data-hold | How long it stays, when looping. Default 1800 ms. |
data-delay | Wait before starting. Default 0 ms. |
Screen readers hear the old word followed by “now” and the new one.
<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><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>