Rapid Word Sequence
A sentence plays one big word at a time, each one cut in with a quick punch, like a fast-cut lyric video. Longer words and punctuation stay on screen a beat longer.
Say it loud, say it fast, make it stick.
How to use it
- Paste the snippet just before
</head>. - Add
data-rapid-word-sequenceto a heading with a short sentence.
Options
Add any of these attributes to the same element.
data-speed | How long each word stays, before extra time for longer words and punctuation. Default 240 ms. |
data-color | The highlight colour some words flash in. Default #a855f7. |
data-loop | Play the sentence again after a pause. Without it, it stops on the last word. |
data-hold | The pause between plays, when looping. Default 900 ms. |
data-delay | Wait before starting. Default 0 ms. |
Screen readers hear the whole sentence at once. Words that are too wide shrink to fit.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Rapid Word Sequence: a sentence plays one big word at a time, each cut
// in with a quick punch, like a fast-cut lyric video. Longer words and
// punctuation stay on screen a beat longer.
// Usage: <h1 data-rapid-word-sequence>Say it loud, say it fast, make it stick.</h1>
// Optional: data-speed (ms each word stays, before extra time for its
// length and punctuation, default 240), data-color (the highlight colour
// some words flash in, default "#a855f7"), data-loop (play the sentence
// again after a pause), data-hold (ms to pause between plays, default
// 900), data-delay (ms). Without data-loop it stops on the last word.
var DEFAULTS = { speed: 240, color: '#a855f7', hold: 900, delay: 0 };
// How each word arrives: a mix of hard cuts and quick moves.
var CUTS = [
{ scale: [1.45, 1], duration: 150 },
{ opacity: [1, 1], duration: 1 },
{ y: ['0.4em', 0], duration: 130 },
{ scale: [0.55, 1], duration: 140 },
{ rotate: [-9, 0], scale: [1.2, 1], duration: 150 },
{ y: ['-0.35em', 0], duration: 130 },
{ opacity: [1, 1], duration: 1 },
{ x: ['0.5em', 0], duration: 120 }
];
// Which words flash in the highlight colour.
var ACCENT = [0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1];
var css =
'.rws-box{display:grid;place-items:center;height:1.3em;max-width:100%;overflow:visible}' +
'.rws-w{display:inline-block;white-space:nowrap;line-height:1.1;will-change:transform}';
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 text = el.textContent.replace(/\s+/g, ' ').trim();
el.setAttribute('aria-label', text);
el.textContent = '';
var box = document.createElement('span');
box.className = 'rws-box';
box.setAttribute('aria-hidden', 'true');
var w = document.createElement('span');
w.className = 'rws-w';
box.appendChild(w);
el.appendChild(box);
return { box: box, w: w, words: text.split(' '), i: 0, visible: false, timer: 0 };
}
function show(el, s, k) {
var d = el.dataset;
var word = s.words[k];
s.w.textContent = word;
s.w.style.color = ACCENT[k % ACCENT.length] ? (d.color || DEFAULTS.color) : '';
// Shrink a word that would not fit across the box.
s.w.style.fontSize = '';
var room = s.box.clientWidth, need = s.w.offsetWidth;
if (need > room) s.w.style.fontSize = (room / need).toFixed(3) + 'em';
var cut = CUTS[(k * 5 + s.i) % CUTS.length];
var props = { ease: 'out(3)' };
Object.keys(cut).forEach(function (p) { props[p] = cut[p]; });
anime.utils.set(s.w, { x: 0, y: 0, scale: 1, rotate: 0, opacity: 1 });
anime.animate(s.w, props);
var speed = +d.speed || DEFAULTS.speed;
var extra = word.length * 18 + (/[,;:–—]$/.test(word) ? 220 : 0) + (/[.!?]$/.test(word) ? 420 : 0);
return speed + extra;
}
function tick(el) {
var s = el._rws, d = el.dataset;
var loop = d.loop !== undefined && d.loop !== 'false';
// Paused off screen: pick up again when it scrolls back in.
if (!s.visible) { s.waiting = true; return; }
if (s.k >= s.words.length) {
if (!loop) return;
s.k = 0;
s.i++;
anime.animate(s.w, { opacity: 0, scale: 0.9, duration: 160, ease: 'in(2)' });
s.timer = setTimeout(function () { tick(el); }, +d.hold || DEFAULTS.hold);
return;
}
var wait = show(el, s, s.k++);
s.timer = setTimeout(function () { tick(el); }, wait);
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var el = e.target, s = el._rws;
s.visible = e.isIntersecting;
if (!s.visible) return;
if (!s.started) {
s.started = true;
s.k = 0;
setTimeout(function () { tick(el); }, +el.dataset.delay || DEFAULTS.delay);
} else if (s.waiting) {
s.waiting = false;
tick(el);
}
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-rapid-word-sequence]').forEach(function (el) {
if (el._rws) return;
if (reduced()) { el._rws = true; return; }
el._rws = setup(el);
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-rapid-word-sequence>Say it loud, say it fast, make it stick.</h1>
<!-- or, slower and looping: -->
<h1 data-rapid-word-sequence data-speed="400" data-loop>Say it loud, say it fast, make it stick.</h1>