Card Flip
A card turns over in 3D to show its back, landing with a little overshoot. Flip it on click, on hover, or let it turn on a timer.
Click to flip
Hello from the back
How to use it
- Paste the snippet just before
</head>. - Add
data-card-flipto a container with two children: the front, then the back.
Options
Add any of these attributes to the same element.
data-trigger | click (default) or hover. |
data-axis | y (default) turns sideways; x tips forwards. |
data-autoplay | Milliseconds: flip on a timer until someone interacts. |
data-duration | Time for a flip. Default 800 ms. |
With the click trigger the card works as a toggle button, including Enter and Space. From code, animacious.flip(card) turns it over.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Card Flip: a card turns over in 3D to show its back, with a little
// overshoot as it lands. Flip it on click, on hover, or on a timer.
// Usage:
// <div data-card-flip>
// <div class="front">Front</div>
// <div class="back">Back</div>
// </div>
// The first child is the front, the second the back.
// Optional: data-trigger ("click", default, or "hover"), data-axis ("y",
// default, turns sideways; "x" tips forwards), data-autoplay (ms between
// flips, until someone interacts), data-duration (ms, default 800).
// Script use: animacious.flip(card) turns it over from code.
var DEFAULTS = { duration: 800 };
var css =
'.cfx{perspective:1200px}' +
'.cfx-inner{display:grid;height:100%;transform-style:preserve-3d}' +
'.cfx-inner>*{grid-area:1/1;backface-visibility:hidden;-webkit-backface-visibility:hidden}' +
'.cfx[data-trigger="hover"]{cursor:default}' +
'.cfx:not([data-trigger="hover"]){cursor:pointer}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function setup(el) {
var d = el.dataset;
var faces = Array.from(el.children);
if (faces.length < 2) return;
el.classList.add('cfx');
var inner = document.createElement('div');
inner.className = 'cfx-inner';
faces.slice(0, 2).forEach(function (f) { inner.appendChild(f); });
el.appendChild(inner);
var prop = d.axis === 'x' ? 'rotateX' : 'rotateY';
var back = {};
back[prop] = 180;
anime.utils.set(faces[1], back);
var s = el._cfx = { inner: inner, faces: faces, prop: prop, flipped: false, timer: null };
faces[1].setAttribute('aria-hidden', 'true');
if (d.trigger === 'hover') {
el.addEventListener('pointerenter', function (e) { if (e.pointerType !== 'touch') { stop(el); flip(el, true); } });
el.addEventListener('pointerleave', function (e) { if (e.pointerType !== 'touch') flip(el, false); });
el.addEventListener('focusin', function () { flip(el, true); });
el.addEventListener('focusout', function () { flip(el, false); });
// Touch screens have no hover: a tap flips it instead.
var touch = false;
el.addEventListener('pointerdown', function (e) { touch = e.pointerType === 'touch'; });
el.addEventListener('click', function () { if (touch) { stop(el); flip(el); } });
} else {
if (!el.hasAttribute('tabindex')) el.tabIndex = 0;
el.setAttribute('role', 'button');
el.setAttribute('aria-pressed', 'false');
if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'Flip card');
el.addEventListener('click', function (e) {
if (e.target.closest('a,button,input,select,textarea') && e.target !== el) return;
stop(el);
flip(el);
});
el.addEventListener('keydown', function (e) {
if (e.target !== el || (e.key !== 'Enter' && e.key !== ' ')) return;
e.preventDefault();
stop(el);
flip(el);
});
}
if (d.autoplay !== undefined && d.autoplay !== 'false' && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
s.timer = setInterval(function () { if (!el.closest('[hidden]')) flip(el); }, +d.autoplay || 2400);
}
}
function stop(el) {
if (el._cfx && el._cfx.timer) { clearInterval(el._cfx.timer); el._cfx.timer = null; }
}
function flip(el, to) {
var s = el._cfx;
if (!s) return;
var next = to === undefined ? !s.flipped : !!to;
if (next === s.flipped) return;
s.flipped = next;
s.faces[0].setAttribute('aria-hidden', next);
s.faces[1].setAttribute('aria-hidden', !next);
if (el.getAttribute('role') === 'button') el.setAttribute('aria-pressed', next);
var props = { duration: +el.dataset.duration || DEFAULTS.duration, ease: 'outBack(1.4)' };
props[s.prop] = next ? 180 : 0;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) props.duration = 0;
else props.scale = [1, 0.94, 1];
anime.animate(s.inner, props);
}
function init() {
document.querySelectorAll('[data-card-flip]').forEach(function (el) {
if (el._cfx) return;
setup(el);
});
}
window.animacious = window.animacious || {};
window.animacious.flip = function (el) { stop(el); flip(el); };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-card-flip>
<div class="front">Front</div>
<div class="back">Back</div>
</div>