Coin Flip
A gold coin lying on the table leaps into the air, spinning over and over, and lands on heads or tails at random with a little bounce and a wobble. Its shadow shrinks as it rises. Click or tap it to toss it again.
How to use it
- Paste the snippet just before
</head>. - Add
data-coin-flipto a<button>with a size. - Put two children in it: heads, then tails. Without them the faces read H and T.
Options
Add any of these attributes to the same element.
data-autoplay | Milliseconds: toss it on a timer while it’s in view, until someone tosses it themselves. |
data-size | Width of the coin in px. Default 42% of the button’s shorter side. |
data-color | Colour of the coin. Default gold, #f5b83d. |
data-duration | Time in the air. Default 1300 ms. |
data-caption="false" | Don’t write “Heads” or “Tails” under it after each toss. |
Each toss is a fair 50:50. The result is put on the button as data-result and sent in a coinflip event: el.addEventListener("coinflip", e => e.detail.result).
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Coin Flip: a coin lying on a table is tossed into the air, spins over
// and over, and lands on heads or tails at random, with a little bounce.
// Its shadow shrinks as it rises. Click or tap it to flip it. Made with
// CSS 3D, with a real edge.
// Usage:
// <button type="button" data-coin-flip>
// <span>H</span> <!-- heads -->
// <span>T</span> <!-- tails -->
// </button>
// Without children the faces read H and T.
// Optional: data-autoplay (ms: flip on a timer while in view, until
// someone flips it themselves), data-size (px, default 42% of the box's
// shorter side), data-color (gold, default #f5b83d), data-duration (ms in
// the air, default 1300), data-caption="false" (no "Heads" / "Tails" line).
// It fires a "coinflip" event with detail.result ("heads" or "tails").
var DEFAULTS = { color: '#f5b83d', duration: 1300 };
var LAYERS = 8; // slices that make the edge
var css =
'.cnf{position:relative;display:grid;place-items:center;perspective:1100px;background:none;border:0;padding:0;font:inherit;color:inherit;cursor:pointer;-webkit-tap-highlight-color:transparent}' +
'.cnf-table{position:relative;width:var(--cnf-s);height:var(--cnf-s);transform-style:preserve-3d;transform:translateY(calc(var(--cnf-s) * .55)) rotateX(55deg)}' +
'.cnf-shadow{position:absolute;inset:0;border-radius:50%;background:radial-gradient(closest-side,rgba(40,10,70,.45),rgba(40,10,70,0));transform:translateZ(-1px)}' +
'.cnf-coin{position:absolute;inset:0;transform-style:preserve-3d}' +
'.cnf-coin>*{position:absolute;inset:0;border-radius:50%;box-sizing:border-box}' +
'.cnf-face{display:grid;place-items:center;font-weight:800;font-size:calc(var(--cnf-s) * .38);line-height:1;color:var(--cnf-ink);backface-visibility:hidden;-webkit-backface-visibility:hidden;' +
'background:radial-gradient(circle at 35% 30%,rgba(255,255,255,.7),rgba(255,255,255,0) 45%),var(--cnf-c);' +
'box-shadow:inset 0 0 0 calc(var(--cnf-s) * .06) var(--cnf-d),inset 0 0 0 calc(var(--cnf-s) * .085) var(--cnf-c),inset 0 0 0 calc(var(--cnf-s) * .095) var(--cnf-d)}' +
'.cnf-face>*{text-shadow:0 1px 0 rgba(255,255,255,.5),0 -1px 0 rgba(0,0,0,.2)}' +
'.cnf-slice{background:var(--cnf-d)}' +
'.cnf-cap{position:absolute;left:0;right:0;bottom:.2em;text-align:center;font-weight:700;font-size:15px;color:var(--text,#222);min-height:1.2em;transition:opacity .25s}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function darken(hex, f) {
hex = hex.replace('#', '');
if (hex.length === 3) hex = hex.replace(/./g, '$&$&');
var n = parseInt(hex, 16);
return 'rgb(' + [n >> 16 & 255, n >> 8 & 255, n & 255].map(function (v) { return Math.round(v * f); }).join(',') + ')';
}
function setup(el) {
var d = el.dataset;
var kids = Array.from(el.children);
el.classList.add('cnf');
var color = d.color || DEFAULTS.color;
el.style.setProperty('--cnf-c', color);
el.style.setProperty('--cnf-d', darken(color, 0.72));
el.style.setProperty('--cnf-ink', darken(color, 0.5));
var table = document.createElement('span');
table.className = 'cnf-table';
table.setAttribute('aria-hidden', 'true');
var shadow = document.createElement('span');
shadow.className = 'cnf-shadow';
var coin = document.createElement('span');
coin.className = 'cnf-coin';
table.appendChild(shadow);
table.appendChild(coin);
// Heads on top, the edge as stacked slices, tails underneath.
var heads = document.createElement('span');
var tails = document.createElement('span');
heads.className = tails.className = 'cnf-face';
heads.appendChild(kids[0] || document.createTextNode('H'));
tails.appendChild(kids[1] || document.createTextNode('T'));
var slices = [];
for (var i = 0; i < LAYERS; i++) {
var sl = document.createElement('span');
sl.className = 'cnf-slice';
coin.appendChild(sl);
slices.push(sl);
}
coin.appendChild(heads);
coin.appendChild(tails);
el.appendChild(table);
var thumb = !!el.closest('[aria-hidden="true"]');
var cap = null;
if (d.caption !== 'false' && !thumb) {
cap = document.createElement('span');
cap.className = 'cnf-cap';
cap.setAttribute('aria-live', 'polite');
el.appendChild(cap);
}
if (el.tagName !== 'BUTTON' && !thumb) {
el.setAttribute('role', 'button');
if (!el.hasAttribute('tabindex')) el.tabIndex = 0;
}
if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', 'Flip the coin');
var px = 0, thick = 0;
var s = { z: 0, spin: 0, wob: 0, busy: false };
function size() {
var box = Math.min(el.clientWidth || 200, el.clientHeight || el.clientWidth || 200);
px = +d.size || Math.round(box * 0.42);
thick = Math.max(3, px * 0.08);
el.style.setProperty('--cnf-s', px + 'px');
heads.style.transform = 'translateZ(' + thick / 2 + 'px)';
tails.style.transform = 'translateZ(' + -thick / 2 + 'px) rotateX(180deg)';
slices.forEach(function (sl, i) {
sl.style.transform = 'translateZ(' + (-thick / 2 + thick * (i + 0.5) / LAYERS).toFixed(2) + 'px)';
});
draw();
}
function draw() {
coin.style.transform = 'translateZ(' + (s.z * px).toFixed(2) + 'px) rotateY(' + s.wob.toFixed(2) + 'deg) rotateX(' + s.spin.toFixed(1) + 'deg)';
var k = 1 / (1 + s.z * 0.9);
shadow.style.transform = 'translateZ(-1px) scale(' + (0.6 + 0.5 * k).toFixed(3) + ')';
shadow.style.opacity = (0.25 + 0.75 * k).toFixed(3);
}
size();
if (window.ResizeObserver) new ResizeObserver(size).observe(el);
var duration = d.duration !== undefined ? +d.duration : DEFAULTS.duration;
function flip() {
if (s.busy) return;
s.busy = true;
var tailsUp = Math.random() < 0.5;
// Whole turns plus a half turn when it should land tails up.
var base = Math.round(s.spin / 360) * 360;
var to = base + 360 * 4 + (tailsUp ? 180 : 0);
if (cap) cap.style.opacity = '0';
if (reduced()) {
s.spin = to; draw(); done(); return;
}
anime.createTimeline({ onUpdate: draw, onComplete: done })
.add(s, { z: 1.7, duration: duration * 0.48, ease: 'out(2)' }, 0)
.add(s, { z: 0, duration: duration * 0.52, ease: 'in(2)' }, duration * 0.48)
.add(s, { spin: to, duration: duration, ease: 'linear' }, 0)
// A small bounce and a wobble as it settles.
.add(s, { z: [0, 0.18, 0], duration: 360, ease: 'out(2)' }, duration)
.add(s, { wob: [0, 9, -5, 2, 0], duration: 700, ease: 'out(2)' }, duration);
function done() {
s.busy = false;
s.spin = to % 360;
draw();
var result = tailsUp ? 'tails' : 'heads';
el.setAttribute('data-result', result);
if (cap) { cap.textContent = tailsUp ? 'Tails' : 'Heads'; cap.style.opacity = '1'; }
el.dispatchEvent(new CustomEvent('coinflip', { detail: { result: result }, bubbles: true }));
}
}
var timer = null;
el.addEventListener('click', function () {
if (timer) { clearInterval(timer); timer = null; }
flip();
});
el.addEventListener('keydown', function (e) {
if (el.tagName === 'BUTTON' || (e.key !== 'Enter' && e.key !== ' ')) return;
e.preventDefault();
el.click();
});
if (d.autoplay !== undefined && d.autoplay !== 'false' && !reduced()) {
var visible = false, first = true;
new IntersectionObserver(function (entries) {
visible = entries[0].isIntersecting;
if (visible && first) { first = false; setTimeout(flip, 500); }
}).observe(el);
timer = setInterval(function () { if (visible) flip(); }, Math.max(duration + 1200, +d.autoplay || 3200));
}
}
function init() {
document.querySelectorAll('[data-coin-flip]').forEach(function (el) {
if (el._cnf) return;
el._cnf = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><button type="button" data-coin-flip style="width: 200px; height: 200px">
<span>★</span>
<span>1</span>
</button>