Character Rotate-in
Letters swing up into place around their baseline, as if hinged at the bottom, tipping just past upright before they settle. Loop it and they tip over backwards before swinging in again.
Swing into view
How to use it
- Paste the snippet just before
</head>. - Add
data-character-rotate-into any heading or text element.
Options
Add any of these attributes to the same element.
data-loop | Swing in, hold, tip over backwards and swing in again. |
data-hold | How long the text stays before tipping over, when looping. Default 1600 ms. |
data-angle | How far forward the letters start tipped, in degrees. Default 100. |
data-duration | How long each letter takes to swing up. Default 1100 ms. |
data-stagger | Gap between letters. Default 40 ms. |
data-delay | Wait before the first letter. Default 0 ms. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Character Rotate-in: letters swing up into place around their baseline,
// as if hinged at the bottom, tipping just past upright before settling.
// Usage: <h1 data-character-rotate-in>Swing into view</h1>
// Optional: data-loop (letters tip over backwards and swing in again),
// data-hold (ms to stay before tipping over), data-angle (degrees letters
// start tipped forward; default 100), data-duration (ms per letter),
// data-stagger (ms between letters), data-delay (ms before starting).
var DEFAULTS = { angle: 100, duration: 1100, stagger: 40, delay: 0, hold: 1600 };
var css =
'.cro-word{display:inline-block;white-space:nowrap;perspective:4em}' +
'.cro-char{display:inline-block;transform-origin:50% 88%;backface-visibility:hidden;' +
'-webkit-backface-visibility:hidden;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 split(el) {
var text = el.textContent.replace(/\s+/g, ' ').trim();
el.setAttribute('aria-label', text);
el.textContent = '';
var chars = [];
text.split(' ').forEach(function (word, i) {
if (i) el.appendChild(document.createTextNode(' '));
var w = document.createElement('span');
w.className = 'cro-word';
w.setAttribute('aria-hidden', 'true');
Array.from(word).forEach(function (c) {
var s = document.createElement('span');
s.className = 'cro-char';
s.textContent = c;
w.appendChild(s);
chars.push(s);
});
el.appendChild(w);
});
return chars;
}
function play(el, chars) {
var d = el.dataset;
var loop = d.loop !== undefined && d.loop !== 'false';
var duration = +d.duration || DEFAULTS.duration;
var stagger = +d.stagger || DEFAULTS.stagger;
var angle = +d.angle || DEFAULTS.angle;
var tl = anime.createTimeline({ loop: loop, loopDelay: 300, delay: +d.delay || DEFAULTS.delay });
tl.add(chars, {
// Negative tips the top of the letter towards the reader.
rotateX: { from: -angle, to: 0, ease: 'outBack(1.6)' },
opacity: { from: 0, to: 1, duration: duration * 0.35, ease: 'linear' },
duration: duration,
delay: anime.stagger(stagger)
});
if (loop) {
// Tip over backwards, away from the reader.
tl.add(chars, {
rotateX: angle,
opacity: 0,
duration: duration * 0.5,
ease: 'in(3)',
delay: anime.stagger(stagger * 0.6)
}, '+=' + (+d.hold || DEFAULTS.hold));
}
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
io.unobserve(e.target);
play(e.target, e.target._croChars);
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-character-rotate-in]').forEach(function (el) {
if (el._croChars) return;
var chars = split(el);
el._croChars = chars;
if (reduced()) return;
anime.utils.set(chars, { opacity: 0 });
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-character-rotate-in>Swing into view</h1>
<!-- or, looping: -->
<h1 data-character-rotate-in data-loop>Swing into view</h1>