Block Wipe Rotator
A solid coloured block sweeps over the word in your sentence, the word changes out of sight underneath, and the block slides off the far side to leave the next word behind. Crisp and editorial.
Make it bold
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-block-wipe-rotator.
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 wipe. Default 1000 ms. |
data-color | The block’s colour. Default the word’s own colour. |
data-direction | right (default) sweeps left to right. left sweeps right to left. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Block Wipe Rotator: a solid coloured block sweeps over the word in your
// sentence, the word changes underneath it, and the block slides off the
// far side to leave the next word behind.
// Usage: <h1>Make it <span data-block-wipe-rotator="bold, loud, yours">bold</span></h1>
// Optional: data-hold (ms per word, default 1800), data-duration (ms per
// wipe, default 1000), data-color (the block, default the text colour),
// data-direction ("right", the default, sweeps left to right; "left" sweeps
// right to left).
var DEFAULTS = { hold: 1800, duration: 1000 };
var css =
'.bwr{display:inline-block;position:relative;white-space:nowrap;vertical-align:baseline;text-align:left}' +
'.bwr-block{position:absolute;left:-.06em;right:-.06em;top:.06em;bottom:.02em;background:currentColor;transform:scaleX(0);pointer-events:none}' +
'.bwr-probe{position:absolute;left:0;top:0;visibility:hidden;white-space:nowrap}' +
'.bwr-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-block-wipe-rotator') || el.textContent)
.split(',').map(function (w) { return w.trim(); }).filter(Boolean);
el.classList.add('bwr');
el.textContent = '';
span('bwr-sr', el, words.join(', '));
var out = span('bwr-word', el, words[0]);
var block = span('bwr-block', el);
var probe = span('bwr-probe', el);
[out, block, probe].forEach(function (n) { n.setAttribute('aria-hidden', 'true'); });
if (el.dataset.color) block.style.background = el.dataset.color;
return { el: el, words: words, out: out, block: block, 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 swap(s, done) {
var d = s.el.dataset;
var t = +d.duration || DEFAULTS.duration;
var fromLeft = d.direction !== 'left';
s.i = (s.i + 1) % s.words.length;
var to = s.words[s.i];
s.busy = true;
// 1. The block grows over the word from one side.
anime.utils.set(s.block, { transformOrigin: fromLeft ? '0% 50%' : '100% 50%' });
anime.animate(s.block, {
scaleX: [0, 1], duration: t * 0.4, ease: 'inOut(3)',
onComplete: function () {
// 2. Hidden under the block, the word changes and the box resizes.
s.out.textContent = to;
var w1 = measure(s, to);
anime.animate(s, {
w: w1, duration: t * 0.2, ease: 'inOut(2)',
onUpdate: function () { s.el.style.width = s.w.toFixed(2) + 'px'; },
onComplete: function () {
// 3. The block shrinks away towards the far side.
anime.utils.set(s.block, { transformOrigin: fromLeft ? '100% 50%' : '0% 50%' });
anime.animate(s.block, {
scaleX: [1, 0], duration: t * 0.4, ease: 'inOut(3)',
onComplete: function () { 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._bwr;
s.visible = e.isIntersecting;
if (s.visible && !s.started) { s.started = true; start(s); }
});
}, { threshold: 0.3 });
document.querySelectorAll('[data-block-wipe-rotator]').forEach(function (el) {
if (el._bwr) return;
el._bwr = setup(el);
if (!reduced && el._bwr.words.length > 1) io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1>
Make it
<span data-block-wipe-rotator="bold, loud, yours">bold</span>
</h1>