Redaction Reveal
Every word starts hidden under a black redaction bar, like a censored document. One by one, in a shuffled order, the bars slide away to uncover the words.
Declassified at last
How to use it
- Paste the snippet just before
</head>. - Add
data-redaction-revealto any heading or paragraph.
Options
Add any of these attributes to the same element.
data-color | The bars. Default #111111. |
data-order | random (default) or sequence, in reading order. |
data-duration | How long each bar takes to slide away. Default 650 ms. |
data-stagger | Gap between bars. Default 160 ms. |
data-loop | Uncover, hold, cover up again and uncover. |
data-hold | How long the words stay uncovered, when looping. Default 1800 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 () {
// Redaction Reveal: every word starts behind a black redaction bar, and the
// bars slide away one after another to uncover the text.
// Usage: <p data-redaction-reveal>Declassified at last</p>
// Optional: data-color (the bars, default "#111111"), data-order (random or
// sequence, default random), data-duration (ms per bar, default 650),
// data-stagger (ms between bars, default 160), data-loop (bars slide back
// over and away again), data-hold (ms to stay uncovered, when looping),
// data-delay (ms before starting).
var DEFAULTS = { color: '#111111', order: 'random', duration: 650, stagger: 160, hold: 1800, delay: 0 };
var css =
'.rdr-w{position:relative;display:inline-block;white-space:nowrap}' +
'.rdr-bar{position:absolute;left:-.06em;right:-.06em;top:.08em;bottom:.04em;border-radius:.04em;' +
'transform-origin:100% 50%;pointer-events:none}';
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();
el.setAttribute('aria-label', text);
el.textContent = '';
var bars = [];
text.split(' ').forEach(function (word, i) {
if (i) el.appendChild(document.createTextNode(' '));
var w = document.createElement('span');
w.className = 'rdr-w';
w.setAttribute('aria-hidden', 'true');
w.textContent = word;
var b = document.createElement('span');
b.className = 'rdr-bar';
b.style.background = d.color || DEFAULTS.color;
w.appendChild(b);
el.appendChild(w);
bars.push(b);
});
return bars;
}
// Delay for each bar: in reading order, or shuffled.
function delays(n, stagger, random) {
var order = [];
for (var i = 0; i < n; i++) order.push(i);
if (random) {
for (var j = n - 1; j > 0; j--) {
var k = (Math.random() * (j + 1)) | 0, t = order[j];
order[j] = order[k]; order[k] = t;
}
}
var out = [];
order.forEach(function (bar, slot) { out[bar] = slot * stagger; });
return out;
}
function slide(el, uncover, done) {
var d = el.dataset, bars = el._rdr;
var duration = +d.duration || DEFAULTS.duration;
var stagger = d.stagger !== undefined ? +d.stagger : DEFAULTS.stagger;
var wait = delays(bars.length, uncover ? stagger : stagger * 0.4, (d.order || DEFAULTS.order) === 'random');
var left = bars.length;
bars.forEach(function (b, i) {
// Bars leave off the right and come back in from the left.
b.style.transformOrigin = uncover ? '100% 50%' : '0% 50%';
anime.animate(b, {
scaleX: uncover ? [1, 0] : [0, 1],
duration: uncover ? duration : duration * 0.6,
delay: wait[i],
ease: uncover ? 'inOutQuart' : 'outQuart',
onComplete: function () { if (--left === 0 && done) done(); }
});
});
}
function play(el) {
var d = el.dataset;
var loop = d.loop !== undefined && d.loop !== 'false';
slide(el, true, function () {
if (!loop) return;
setTimeout(function () {
slide(el, false, function () { setTimeout(function () { play(el); }, 600); });
}, +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); }, 300 + (+el.dataset.delay || DEFAULTS.delay));
});
}, { threshold: 0.3 });
document.querySelectorAll('[data-redaction-reveal]').forEach(function (el) {
if (el._rdr) return;
el._rdr = setup(el);
if (reduced()) {
el._rdr.forEach(function (b) { b.style.transform = 'scaleX(0)'; });
return;
}
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><p data-redaction-reveal>The meeting took place on Tuesday.</p>
<!-- or, in reading order: -->
<h1 data-redaction-reveal data-order="sequence">Declassified at last</h1>