Word Flip
The word in your sentence flips over on its horizontal axis, like a sign turned on a hinge, to show the next word on its back. It lifts towards you as it turns and lands with a small bounce.
Ideas that stick
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-flip.
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 flip. Default 900 ms. |
data-direction | up (default): the top edge tips away from you. down: it tips towards you. |
The word is as wide as the longest one, so the sentence never shifts.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Word Flip: the word in your sentence flips over on its horizontal axis,
// like a sign turned on a hinge, to show the next word on its back. It
// lifts slightly as it turns and lands with a small bounce.
// Usage: <h1>Ideas that <span data-word-flip="stick, sell, scale">stick</span></h1>
// Optional: data-hold (ms per word, default 1800), data-duration (ms per
// flip, default 900), data-direction ("up" (default): the top edge tips
// away from you; "down": it tips towards you).
var DEFAULTS = { hold: 1800, duration: 900 };
var css =
'.wfl{display:inline-grid;position:relative;vertical-align:baseline;text-align:left;perspective:5em}' +
'.wfl-size{grid-area:1/1;visibility:hidden;white-space:nowrap}' +
'.wfl-card{position:absolute;inset:0;transform-style:preserve-3d}' +
'.wfl-face{position:absolute;inset:0;white-space:nowrap;backface-visibility:hidden;-webkit-backface-visibility:hidden}' +
'.wfl-back{transform:rotateX(180deg)}' +
'.wfl-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 !== undefined) s.textContent = text;
parent.appendChild(s);
return s;
}
function build(el) {
var words = (el.getAttribute('data-word-flip') || el.textContent)
.split(',').map(function (w) { return w.trim(); }).filter(Boolean);
el.classList.add('wfl');
el.textContent = '';
span('wfl-sr', el, words.join(', '));
// Hidden copies of every word: the box takes the widest one and sits on the
// sentence's baseline, so nothing around it shifts.
words.forEach(function (w) { span('wfl-size', el, w).setAttribute('aria-hidden', 'true'); });
var card = span('wfl-card', el);
card.setAttribute('aria-hidden', 'true');
var faces = [span('wfl-face', card, words[0]), span('wfl-face wfl-back', card, '')];
return { el: el, words: words, card: card, faces: faces, step: 0 };
}
function start(s) {
var d = s.el.dataset;
var dir = d.direction === 'down' ? -1 : 1;
var duration = +d.duration || DEFAULTS.duration;
(function next() {
s.step++;
// The face turning away from you gets the next word written on it.
s.faces[s.step % 2].textContent = s.words[s.step % s.words.length];
anime.createTimeline({ delay: +d.hold || DEFAULTS.hold, onComplete: next })
.add(s.card, { rotateX: dir * s.step * 180, duration: duration, ease: 'outBack(1.7)' }, 0)
// A little lift towards you mid-flip, so it feels like a real card.
.add(s.card, { z: [{ to: '0.35em', duration: duration * 0.35, ease: 'out(2)' }, { to: 0, duration: duration * 0.5, ease: 'inOut(2)' }] }, 0)
// The faces darken slightly as they turn edge-on to the light.
.add(s.faces, { filter: ['brightness(1)', 'brightness(0.6)', 'brightness(1)'], duration: duration * 0.65, ease: 'inOut(2)' }, 0);
})();
}
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._wfl);
});
}, { threshold: 0.3 });
document.querySelectorAll('[data-word-flip]').forEach(function (el) {
if (el._wfl) return;
el._wfl = build(el);
// Reduced motion: the first word simply stays.
if (!reduced && el._wfl.words.length > 1) io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1>
Ideas that
<span data-word-flip="stick, sell, scale">stick</span>
</h1>