Glitch Text
Every few seconds the text breaks up for a split second: red and cyan slices jump sideways out of it and the line judders, like a corrupted video signal. Then it snaps back as if nothing happened.
Signal lost
How to use it
- Paste the snippet just before
</head>. - Add
data-glitch-textto any heading or text element.
Options
Add any of these attributes to the same element.
data-trigger | view (default) glitches every few seconds while on screen. hover glitches when the pointer arrives. |
data-interval | Time between glitches. Default 2400 ms. |
data-duration | How long each glitch lasts. Default 500 ms. |
data-strength | How far the slices jump. Default 1; try 2 for a harsher glitch. |
data-colors | The two split colours, comma separated. Default #ff2d6f, #00c8ff. |
Short, bold, uppercase words glitch best. Screen readers get the text once, without the coloured copies.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Glitch Text: every so often the text breaks up for a moment, with red and
// cyan slices jumping sideways out of it, like a corrupted video signal.
// Keeps going while the text is on screen.
// Usage: <h1 data-glitch-text>System error</h1>
// Optional: data-trigger ("view", the default, glitches every few seconds
// while on screen; "hover" glitches when the pointer arrives), data-interval
// (ms between glitches; default 2400), data-duration (ms each glitch lasts;
// default 500), data-strength (how far slices jump; default 1), data-colors
// (the two split colours, comma separated; default "#ff2d6f, #00c8ff").
var DEFAULTS = { interval: 2400, duration: 500, strength: 1, colors: '#ff2d6f, #00c8ff' };
var FRAME = 50; // ms each jumbled frame is shown for
var css =
'.gli-box{position:relative;display:inline-block}' +
'.gli-layer{position:absolute;inset:0;opacity:0;pointer-events:none;white-space:inherit;user-select:none}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
// Wraps the text in an inline box and lays two coloured copies over it.
function build(el) {
var d = el.dataset;
var colors = (d.colors || DEFAULTS.colors).split(',');
var box = document.createElement('span');
box.className = 'gli-box';
while (el.firstChild) box.appendChild(el.firstChild);
el.appendChild(box);
var text = box.textContent;
var layers = colors.slice(0, 2).map(function (c) {
var layer = document.createElement('span');
layer.className = 'gli-layer';
layer.setAttribute('aria-hidden', 'true');
layer.textContent = text;
layer.style.color = c.trim();
box.appendChild(layer);
return layer;
});
return { box: box, layers: layers };
}
// A random horizontal slice of the text, as a clip-path.
function slice() {
var top = anime.utils.random(0, 85);
var height = anime.utils.random(8, 38);
return 'inset(' + top + '% -10% ' + Math.max(0, 100 - top - height) + '% -10%)';
}
function glitch(el, parts) {
var d = el.dataset;
var px = parseFloat(getComputedStyle(el).fontSize) * 0.12 * (+d.strength || DEFAULTS.strength);
var frames = Math.max(2, Math.round((+d.duration || DEFAULTS.duration) / FRAME));
var tl = anime.createTimeline({ autoplay: false });
for (var i = 0; i < frames; i++) {
var t = i * FRAME;
// Every few frames the slices drop out, so it stutters instead of buzzing.
var off = i % 4 === 2;
parts.layers.forEach(function (layer, n) {
var dir = n ? 1 : -1;
tl.set(layer, {
opacity: off ? 0 : 1,
clipPath: slice(),
x: dir * anime.utils.random(px * 0.3, px, 1)
}, t);
});
tl.set(parts.box, {
x: anime.utils.random(-px, px, 1) * 0.35,
skewX: Math.random() < 0.25 ? anime.utils.random(-10, 10) : 0
}, t);
}
tl.set(parts.layers, { opacity: 0, x: 0 }, frames * FRAME)
.set(parts.box, { x: 0, skewX: 0 }, frames * FRAME);
tl.play();
return tl;
}
function init() {
if (reduced()) return;
document.querySelectorAll('[data-glitch-text]').forEach(function (el) {
if (el._gli) return;
var parts = (el._gli = build(el));
var d = el.dataset;
var busy = false;
function once() {
if (busy) return;
busy = true;
glitch(el, parts).then(function () { busy = false; });
}
if (d.trigger === 'hover') {
el.addEventListener('pointerenter', once);
return;
}
// Only glitch while visible, and start with one as it scrolls in.
var timer = null;
var interval = +d.interval || DEFAULTS.interval;
new IntersectionObserver(function (entries) {
clearInterval(timer);
if (!entries[0].isIntersecting) return;
setTimeout(once, 300);
timer = setInterval(function () {
once();
// Sometimes a second, quick aftershock.
if (Math.random() < 0.4) setTimeout(once, (+d.duration || DEFAULTS.duration) + 150);
}, interval);
}, { threshold: 0.3 }).observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-glitch-text>Signal lost</h1>
<!-- on hover, e.g. a nav link: -->
<a href="/play" data-glitch-text data-trigger="hover">Play</a>