Word Carousel
Words roll vertically through a slot in your sentence, like a drum turning: the next word tips up from below while the last one tilts away above, with faded neighbours peeking out. The slot eases to each word’s width, so the sentence closes up around it.
Build faster
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-word-carousel.
Options
Add any of these attributes to the same element.
data-hold | How long each word stays. Default 1600 ms. |
data-duration | Time for each roll. Default 700 ms. |
data-peek | Opacity of the words peeking above and below. Default 0.2; 0 hides them. |
The neighbours peek outside the line, so leave a little space above and below the headline (or set data-peek="0").
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Word Carousel: words roll vertically through a slot in your sentence, like
// a drum turning: the next word tips up from below while the last one tilts
// away above, with faded neighbours peeking out. The slot eases to the
// width of each word, so the sentence closes up around it.
// Usage: <h1>Build <span data-word-carousel="faster, better, together">faster</span></h1>
// Optional: data-hold (ms per word, default 1600), data-duration (ms per
// roll, default 700), data-peek (opacity of the words above and below,
// default 0.2; 0 hides them).
var DEFAULTS = { hold: 1600, duration: 700, peek: 0.2 };
var css =
'.wcr{display:inline-grid;position:relative;vertical-align:baseline;text-align:left;perspective:6em}' +
'.wcr-size{grid-area:1/1;visibility:hidden;white-space:nowrap}' +
'.wcr-word{position:absolute;left:0;top:0;white-space:nowrap;will-change:transform,opacity}' +
'.wcr-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;
s.textContent = text;
parent.appendChild(s);
return s;
}
function build(el, peek) {
var words = (el.getAttribute('data-word-carousel') || el.textContent)
.split(',').map(function (w) { return w.trim(); }).filter(Boolean);
el.classList.add('wcr');
el.textContent = '';
span('wcr-sr', el, words.join(', '));
// An invisible copy of the current word holds the slot open and keeps the
// sentence's baseline; the visible words float over it.
var size = span('wcr-size', el, words[0]);
size.setAttribute('aria-hidden', 'true');
var items = words.map(function (w) {
var s = span('wcr-word', el, w);
s.setAttribute('aria-hidden', 'true');
return s;
});
var state = { el: el, words: words, items: items, size: size, pos: 0, peek: peek, width: 0 };
render(state);
return state;
}
// Lay the words round the drum for a position: 0 is the first word in the
// slot, 1.5 is halfway between the second and third, and so on.
function render(s) {
var n = s.items.length;
s.items.forEach(function (item, i) {
// Distance from the slot, wrapped so the last word sits just above the first.
var d = ((((i - s.pos) % n) + n + n / 2) % n) - n / 2;
var a = Math.abs(d);
item.style.transform =
'translateY(' + (d * 92).toFixed(2) + '%) rotateX(' + (-d * 38).toFixed(2) + 'deg) scale(' + (1 - Math.min(a, 1.5) * 0.12).toFixed(3) + ')';
item.style.opacity = a < 1 ? (1 - a * (1 - s.peek)).toFixed(3) : Math.max(0, s.peek * (2 - a)).toFixed(3);
});
}
function start(s) {
var d = s.el.dataset;
var duration = +d.duration || DEFAULTS.duration;
var step = 0;
s.width = s.items[0].offsetWidth;
s.el.style.width = s.width + 'px';
render(s);
(function next() {
step++;
var to = s.items[step % s.words.length];
s.size.textContent = to.textContent;
anime.animate(s, {
pos: step,
width: to.offsetWidth,
duration: duration,
delay: +d.hold || DEFAULTS.hold,
ease: 'outBack(1.4)',
onUpdate: function () {
s.el.style.width = s.width.toFixed(2) + 'px';
render(s);
},
onComplete: next
});
})();
}
function init() {
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
io.unobserve(e.target);
start(e.target._wcr);
});
}, { threshold: 0.3 });
document.querySelectorAll('[data-word-carousel]').forEach(function (el) {
if (el._wcr) return;
var peek = el.dataset.peek !== undefined ? +el.dataset.peek : DEFAULTS.peek;
// Reduced motion: just the first word, without its neighbours.
el._wcr = build(el, reduced ? 0 : peek);
if (!reduced && el._wcr.words.length > 1) io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1>
Build
<span data-word-carousel="faster, better, together">faster</span>
</h1>