Glitch Word Swap
The word in your sentence glitches into the next one. Red and cyan copies split off, slices of the word jump sideways, and the letters flicker between old and new until the new word locks in.
Hack the planet
How to use it
- Paste the snippet just before
</head>. - Wrap the changing word in a
<span>and list every word, separated by commas, indata-glitch-word-swap.
Options
Add any of these attributes to the same element.
data-hold | How long each word stays. Default 1800 ms. |
data-duration | Time for each glitch. Default 650 ms. |
data-colors | Two colours for the split copies, separated by a comma. Default #ff2e63, #08d9d6. |
data-intensity | How far the copies and slices jump, in em. Default 0.08. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Glitch Word Swap: the word in your sentence glitches into the next one.
// Red and cyan copies split off, slices of the word jump sideways, and the
// letters flicker between the old word and the new until the new one wins.
// Usage: <h1>Hack the <span data-glitch-word-swap="planet, system, future">planet</span></h1>
// Optional: data-hold (ms per word, default 1800), data-duration (ms per
// glitch, default 650), data-colors (two colours for the split copies,
// default "#ff2e63, #08d9d6"), data-intensity (how far slices jump, in em,
// default 0.08).
var DEFAULTS = { hold: 1800, duration: 650, colors: '#ff2e63, #08d9d6', intensity: 0.08 };
var css =
'.gws{display:inline-block;position:relative;white-space:nowrap;vertical-align:baseline;text-align:left}' +
'.gws-word{position:relative;z-index:1;display:inline-block}' +
'.gws-ghost{position:absolute;z-index:0;left:0;top:0;white-space:nowrap;pointer-events:none;opacity:0}' +
'.gws-on .gws-ghost{opacity:.9}' +
'.gws-probe{position:absolute;left:0;top:0;visibility:hidden;white-space:nowrap}' +
'.gws-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 span(cls, parent, text) {
var s = document.createElement('span');
s.className = cls;
if (text) s.textContent = text;
parent.appendChild(s);
return s;
}
function setup(el) {
var words = (el.getAttribute('data-glitch-word-swap') || el.textContent)
.split(',').map(function (w) { return w.trim(); }).filter(Boolean);
var colors = (el.dataset.colors || DEFAULTS.colors).split(',').map(function (c) { return c.trim(); });
el.classList.add('gws');
el.textContent = '';
span('gws-sr', el, words.join(', '));
var out = span('gws-word', el, words[0]);
var a = span('gws-ghost', el, words[0]);
var b = span('gws-ghost', el, words[0]);
el.insertBefore(a, out);
el.insertBefore(b, out);
a.style.color = colors[0];
b.style.color = colors[1] || colors[0];
var probe = span('gws-probe', el);
[out, a, b, probe].forEach(function (n) { n.setAttribute('aria-hidden', 'true'); });
return { el: el, words: words, out: out, a: a, b: b, probe: probe, i: 0, w: 0, busy: false, visible: false, started: false };
}
function measure(s, text) {
s.probe.textContent = text;
return s.probe.offsetWidth;
}
function fit(s) {
if (s.busy) return;
s.w = measure(s, s.words[s.i]);
s.el.style.width = s.w + 'px';
}
function rnd(a, b) { return a + Math.random() * (b - a); }
// A random horizontal band of the word, as a clip-path.
function slice() {
var top = rnd(0, 80);
return 'inset(' + top.toFixed(1) + '% -10% ' + Math.max(0, 100 - top - rnd(8, 40)).toFixed(1) + '% -10%)';
}
// Mix the two words letter by letter: each letter is the new one with
// probability p, so the word flickers and settles.
function mix(from, to, p) {
var len = Math.max(from.length, to.length), outText = '';
for (var i = 0; i < len; i++) outText += Math.random() < p ? (to[i] || '') : (from[i] || '');
return outText;
}
function swap(s, done) {
var d = s.el.dataset;
var amp = +d.intensity || DEFAULTS.intensity;
var from = s.words[s.i];
s.i = (s.i + 1) % s.words.length;
var to = s.words[s.i];
var w0 = s.w, w1 = measure(s, to);
var state = { p: 0 }, last = -1;
s.busy = true;
s.el.classList.add('gws-on');
anime.animate(state, {
p: 1,
duration: +d.duration || DEFAULTS.duration,
ease: 'linear',
onUpdate: function () {
var p = state.p;
s.w = w0 + (w1 - w0) * (p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2);
s.el.style.width = s.w.toFixed(2) + 'px';
// Jump to a new glitch frame roughly every 45 ms.
var frame = Math.floor(p * (+d.duration || DEFAULTS.duration) / 45);
if (frame === last) return;
last = frame;
// Strongest in the middle, calm at both ends.
var k = Math.sin(Math.PI * p);
var text = mix(from, to, Math.min(1, Math.max(0, (p - 0.2) / 0.6)));
if (p > 0.85) text = to;
s.out.textContent = text;
s.a.textContent = Math.random() < 0.5 ? from : to;
s.b.textContent = text;
s.out.style.transform = 'translateX(' + (rnd(-1, 1) * amp * k * 0.6).toFixed(3) + 'em)';
s.out.style.clipPath = Math.random() < 0.3 * k ? slice() : '';
s.a.style.transform = 'translate(' + (-amp * k * rnd(0.5, 1.6)).toFixed(3) + 'em,' + (rnd(-0.3, 0.3) * amp).toFixed(3) + 'em)';
s.b.style.transform = 'translate(' + (amp * k * rnd(0.5, 1.6)).toFixed(3) + 'em,' + (rnd(-0.3, 0.3) * amp).toFixed(3) + 'em)';
s.a.style.clipPath = slice();
s.b.style.clipPath = slice();
},
onComplete: function () {
s.out.textContent = to;
s.a.textContent = s.b.textContent = to;
s.out.style.transform = s.out.style.clipPath = '';
s.el.classList.remove('gws-on');
s.w = w1;
s.el.style.width = w1 + 'px';
s.busy = false;
done();
}
});
}
// Wait, then run fn, but only once the word is on screen.
function wait(s, ms, fn) {
setTimeout(function check() {
if (s.visible && !document.hidden) fn();
else setTimeout(check, 300);
}, ms);
}
function start(s) {
fit(s);
if (document.fonts) document.fonts.ready.then(function () { fit(s); });
window.addEventListener('resize', function () { fit(s); });
var hold = +s.el.dataset.hold || DEFAULTS.hold;
(function loop() {
wait(s, hold, function () { swap(s, loop); });
})();
}
function init() {
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var s = e.target._gws;
s.visible = e.isIntersecting;
if (s.visible && !s.started) { s.started = true; start(s); }
});
}, { threshold: 0.3 });
document.querySelectorAll('[data-glitch-word-swap]').forEach(function (el) {
if (el._gws) return;
el._gws = setup(el);
if (!reduced && el._gws.words.length > 1) io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1>
Hack the
<span data-glitch-word-swap="planet, system, future">planet</span>
</h1>