Character 3D Flip
Letters turn over like cards, rippling out from the middle of the word, to show a back face in another colour, or another letter. Play it on scroll, on a loop, or when the pointer is over the text.
Flip it
How to use it
- Paste the snippet just before
</head>. - Add
data-character-flipto any heading, link or text element.
Options
Add any of these attributes to the same element.
data-trigger | view (default) flips when scrolled into view. hover flips while the pointer is over the text. |
data-loop | Keep flipping back and forth (with the view trigger). |
data-back-color | Colour of the back faces. Default #a855f7. |
data-back | Different text for the back, letter by letter, e.g. data-back="NEW" on “OLD”. |
data-from | Where the ripple starts: center (default), first or last. |
data-hold | How long each side stays when looping. Default 1500 ms. |
data-duration | How long each letter takes to turn. Default 700 ms. |
data-stagger | Gap between letters. Default 45 ms. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Character 3D Flip: letters turn over like cards, rippling out from the
// middle of the word, to show a back face in another colour (or letter).
// Usage: <h1 data-character-flip>Flip it</h1>
// Optional: data-trigger ("view", the default, or "hover"), data-loop,
// data-back-color (default #a855f7), data-back (other text for the back),
// data-from ("center", "first", "last"), data-hold, data-duration,
// data-stagger (all ms).
var DEFAULTS = { duration: 700, stagger: 45, hold: 1500, backColor: '#a855f7', from: 'center' };
var css =
'.chf-word{display:inline-block;white-space:nowrap}' +
// Both faces share one grid cell, so a letter is as wide as its wider face.
'.chf-char{display:inline-grid;transform-style:preserve-3d}' +
'.chf-face{grid-area:1/1;text-align:center;backface-visibility:hidden;-webkit-backface-visibility:hidden}' +
'.chf-back{transform:rotateY(180deg)}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function split(el) {
var text = el.textContent.replace(/\s+/g, ' ').trim();
var back = Array.from((el.dataset.back || '').replace(/\s+/g, ' ').trim());
var color = el.dataset.backColor || DEFAULTS.backColor;
el.setAttribute('aria-label', text);
el.textContent = '';
var chars = [];
var i = 0;
text.split(' ').forEach(function (word, wi) {
if (wi) { el.appendChild(document.createTextNode(' ')); i++; }
var w = document.createElement('span');
w.className = 'chf-word';
w.setAttribute('aria-hidden', 'true');
Array.from(word).forEach(function (c) {
var ch = document.createElement('span');
ch.className = 'chf-char';
var front = document.createElement('span');
front.className = 'chf-face';
front.textContent = c;
var b = document.createElement('span');
b.className = 'chf-face chf-back';
b.style.color = color;
b.textContent = back[i] && back[i] !== ' ' ? back[i] : c;
ch.appendChild(front);
ch.appendChild(b);
w.appendChild(ch);
chars.push(ch);
i++;
});
el.appendChild(w);
});
return chars;
}
function setup(el) {
var chars = split(el);
var d = el.dataset;
var opts = {
duration: +d.duration || DEFAULTS.duration,
stagger: +d.stagger || DEFAULTS.stagger,
hold: +d.hold || DEFAULTS.hold,
from: d.from || DEFAULTS.from
};
// Each letter has its own perspective so it turns about its own centre.
anime.utils.set(chars, { perspective: '500px' });
var anim = null;
function flipTo(side, wait, then) {
if (anim) anim.pause();
anim = anime.animate(chars, {
rotateY: side * 180,
duration: opts.duration,
delay: anime.stagger(opts.stagger, { from: opts.from, start: wait || 0 }),
ease: 'outBack(1.4)',
onComplete: then
});
}
return { el: el, chars: chars, opts: opts, flipTo: flipTo };
}
function playInView(f) {
var loop = f.el.dataset.loop !== undefined && f.el.dataset.loop !== 'false';
var side = 0;
(function next() {
side = side ? 0 : 1;
f.flipTo(side, side === 1 && !loop ? 0 : f.opts.hold, loop ? next : undefined);
})();
}
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);
playInView(e.target._chf);
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-character-flip]').forEach(function (el) {
if (el._chf) return;
var f = (el._chf = setup(el));
if (reduced) return;
if (el.dataset.trigger === 'hover') {
el.addEventListener('mouseenter', function () { f.flipTo(1); });
el.addEventListener('mouseleave', function () { f.flipTo(0); });
el.addEventListener('focus', function () { f.flipTo(1); });
el.addEventListener('blur', function () { f.flipTo(0); });
} else {
io.observe(el);
}
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-character-flip data-loop>Flip it</h1>
<!-- flip on hover, different letters on the back: -->
<a href="/sale" data-character-flip data-trigger="hover" data-back="SHOP">SALE</a>