Glitch Reveal
The text jitters into view through a burst of RGB-split glitches, torn slices and flicker, as if the signal were tuning in, then snaps clean and holds perfectly still.
Signal found
How to use it
- Paste the snippet just before
</head>. - Add
data-glitch-revealto any heading or text element.
Options
Add any of these attributes to the same element.
data-colors | The two split colours, comma-separated. Default #ff2a6d,#05d9e8. |
data-duration | How long the glitching lasts. Default 1000 ms. |
data-loop | Glitch in, hold, glitch out and in again. |
data-hold | How long the text stays clean before glitching out, when looping. Default 2200 ms. |
data-delay | Wait before starting. Default 0 ms. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Glitch Reveal: text jitters into view through a burst of RGB-split
// glitches, torn slices and flicker, then snaps clean and holds still.
// Usage: <h1 data-glitch-reveal>Signal found</h1>
// Optional: data-colors (the two split colours, default "#ff2a6d,#05d9e8"),
// data-duration (ms of glitching, default 1000), data-loop (glitch out, then
// in again), data-hold (ms to stay clean before glitching out), data-delay
// (ms before starting).
var DEFAULTS = { colors: '#ff2a6d,#05d9e8', duration: 1000, hold: 2200, delay: 0 };
var css =
'.glr-on{position:relative;isolation:isolate}' +
'.glr-main{display:block;position:relative;z-index:1}' +
'.glr-ghost{position:absolute;inset:0;z-index:0;pointer-events:none;opacity:0}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
var R = function (a, b) { return a + Math.random() * (b - a); };
function reduced() {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
function setup(el) {
var d = el.dataset, cs = getComputedStyle(el);
var text = el.textContent.replace(/\s+/g, ' ').trim();
var colors = (d.colors || DEFAULTS.colors).split(',');
el.setAttribute('aria-label', text);
el.textContent = '';
el.classList.add('glr-on');
var ghosts = colors.slice(0, 2).map(function (c) {
var g = document.createElement('span');
g.className = 'glr-ghost';
g.setAttribute('aria-hidden', 'true');
g.textContent = text;
g.style.color = c.trim();
g.style.padding = cs.padding;
el.appendChild(g);
return g;
});
var main = document.createElement('span');
main.className = 'glr-main';
main.setAttribute('aria-hidden', 'true');
main.textContent = text;
el.appendChild(main);
return { main: main, ghosts: ghosts, fs: parseFloat(cs.fontSize) };
}
// A random horizontal band, as an inset() clip.
function band() {
var top = R(0, 85);
return 'inset(' + top.toFixed(0) + '% 0 ' + Math.max(0, 100 - top - R(8, 40)).toFixed(0) + '% 0)';
}
function clean(s) {
s.main.style.cssText = '';
s.ghosts.forEach(function (g) { g.style.opacity = 0; g.style.transform = ''; g.style.clipPath = ''; });
}
// One glitch frame. `q` runs 0 (all noise) to 1 (settled).
function frame(s, q) {
var amt = (1 - q) * s.fs;
var show = Math.random() < 0.25 + q * 0.85;
s.main.style.opacity = show ? (Math.random() < 0.2 ? 0.6 : 1) : 0;
s.main.style.transform = 'translate(' + (R(-0.06, 0.06) * amt).toFixed(1) + 'px,0) skewX(' + R(-8, 8) * (1 - q) + 'deg)';
s.main.style.clipPath = Math.random() < 0.45 * (1 - q) ? band() : '';
s.ghosts.forEach(function (g, i) {
var on = Math.random() < 0.9 - q * 0.4;
g.style.opacity = on ? 0.9 : 0;
var dir = i ? 1 : -1;
g.style.transform = 'translate(' + (dir * R(0.03, 0.14) * amt).toFixed(1) + 'px,' + (R(-0.03, 0.03) * amt).toFixed(1) + 'px)';
g.style.clipPath = Math.random() < 0.6 ? band() : '';
});
}
// Runs glitch frames for `duration` ms at about 20 fps; `out` plays it
// backwards, ending hidden.
function burst(el, out, done) {
var s = el._glr, d = el.dataset;
var duration = +d.duration || DEFAULTS.duration;
var start = performance.now();
(function tick() {
var q = Math.min(1, (performance.now() - start) / duration);
if (q >= 1) {
clean(s);
if (out) s.main.style.opacity = 0;
else {
// A last little twitch as it snaps clean.
s.main.style.textShadow = (-0.03 * s.fs) + 'px 0 ' + s.ghosts[0].style.color + ',' +
(0.03 * s.fs) + 'px 0 ' + (s.ghosts[1] || s.ghosts[0]).style.color;
setTimeout(function () { s.main.style.textShadow = ''; }, 70);
}
return done && done();
}
frame(s, out ? 1 - q : Math.pow(q, 1.4));
setTimeout(tick, R(35, 75));
})();
}
function play(el) {
var d = el.dataset;
var loop = d.loop !== undefined && d.loop !== 'false';
burst(el, false, function () {
if (!loop) return;
setTimeout(function () {
burst(el, true, function () { setTimeout(function () { play(el); }, 500); });
}, +d.hold || DEFAULTS.hold);
});
}
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.3 });
document.querySelectorAll('[data-glitch-reveal]').forEach(function (el) {
if (el._glr) return;
el._glr = setup(el);
if (reduced()) return;
el._glr.main.style.opacity = 0;
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-glitch-reveal>Signal found</h1>
<!-- or, with yellow and blue splits, looping: -->
<h1 data-glitch-reveal data-colors="#facc15,#3b82f6" data-loop>Signal found</h1>