Flip Clock
A live clock on dark flip cards, one for the hours and one for the minutes. When the time changes, the top half of the card falls forward and the new number swings down and settles with a little bounce. Add a small seconds card to watch it flip every second.
How to use it
- Paste the snippet just before
</head>. - Add
data-flip-clockto an empty element and size it withfont-size. It shows the visitor’s local time.
Options
Add any of these attributes to the same element.
data-seconds | Add a small seconds card that flips every second. |
data-12 | Use a 12-hour clock, with AM or PM on the hour card. |
data-time-zone | Show another place’s time, e.g. Europe/London or Asia/Tokyo. |
data-card | Card colour. Default #18181b. |
data-ink | Digit colour. Default #f4f4f5. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Flip Clock: a live clock on dark flip cards, one card for the hours and
// one for the minutes. When a value changes the top half of the card falls
// forward and the new number swings down and settles.
// Usage: <div data-flip-clock></div> (size it with font-size)
// Optional: data-seconds (add a small seconds card that flips every second),
// data-12 (12-hour clock with AM/PM), data-time-zone (an IANA zone such as
// "Europe/London"; default the visitor's own), data-card (card colour,
// default #18181b), data-ink (digit colour, default #f4f4f5).
var DEFAULTS = { card: '#18181b', ink: '#f4f4f5' };
var css =
'.flc{--flc-card:#18181b;--flc-ink:#f4f4f5;display:inline-flex;align-items:flex-end;gap:0.1em;font-weight:700;line-height:1;' +
'font-variant-numeric:tabular-nums;letter-spacing:-0.02em}' +
'.flc-card{position:relative;width:1.36em;height:1.5em;perspective:5em;border-radius:0.11em;flex:none;' +
'box-shadow:0 0.05em 0.1em rgba(0,0,0,.18),0 0.2em 0.5em rgba(0,0,0,.16)}' +
'.flc-card>span{position:absolute;inset:0;display:grid;place-items:center;border-radius:inherit;background:var(--flc-card);color:var(--flc-ink);' +
'backface-visibility:hidden;-webkit-backface-visibility:hidden}' +
'.flc-top,.flc-ftop{clip-path:inset(0 0 50% 0)}' +
'.flc-bot,.flc-fbot{clip-path:inset(50% 0 0 0)}' +
'.flc-top,.flc-ftop{background-image:linear-gradient(rgba(255,255,255,.05),rgba(255,255,255,0))!important}' +
'.flc-ftop,.flc-fbot{z-index:2;box-shadow:inset 0 0 0 999px rgba(0,0,0,var(--flc-shade,0))}' +
'.flc-card::after{content:"";position:absolute;left:0;right:0;top:50%;height:0.025em;min-height:1px;transform:translateY(-50%);background:rgba(0,0,0,.55);z-index:3}' +
'.flc-card.flc-sec{font-size:0.4em;margin-left:0.1em}' +
'.flc-ampm{position:absolute;left:0.5em;top:0.45em;font-size:0.13em;font-weight:700;letter-spacing:0.08em;color:var(--flc-ink);opacity:.7;z-index:4}' +
'.flc-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 reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function make(tag, cls, parent, text) {
var n = document.createElement(tag);
n.className = cls;
if (text !== undefined) n.textContent = text;
parent.appendChild(n);
return n;
}
function two(n) { return (n < 10 ? '0' : '') + n; }
// The time now, in the chosen zone if there is one.
function now(zone) {
var d = new Date();
if (!zone) return { h: d.getHours(), m: d.getMinutes(), s: d.getSeconds() };
try {
var p = {};
new Intl.DateTimeFormat('en-GB', { timeZone: zone, hour: 'numeric', minute: 'numeric', second: 'numeric', hourCycle: 'h23' })
.formatToParts(d).forEach(function (x) { p[x.type] = +x.value; });
return { h: p.hour % 24, m: p.minute, s: p.second };
} catch (e) {
return { h: d.getHours(), m: d.getMinutes(), s: d.getSeconds() };
}
}
function makeCard(parent, cls) {
var card = make('span', 'flc-card' + (cls ? ' ' + cls : ''), parent);
var parts = {};
['top', 'bot', 'ftop', 'fbot'].forEach(function (k) { parts[k] = make('span', 'flc-' + k, card, '00'); });
anime.utils.set(parts.fbot, { rotateX: 90 });
return { el: card, parts: parts, value: '00', busy: false, queued: null };
}
// The top half of the old number falls forward, uncovering the new one,
// then the bottom half of the new number swings down and bounces to rest.
function flip(card, next) {
var p = card.parts;
var old = card.value;
card.value = next;
card.busy = true;
p.top.textContent = next;
p.fbot.textContent = next;
p.ftop.textContent = old;
p.bot.textContent = old;
anime.createTimeline({
onComplete: function () {
p.bot.textContent = next;
p.ftop.textContent = next;
anime.utils.set(p.ftop, { rotateX: 0, '--flc-shade': 0 });
anime.utils.set(p.fbot, { rotateX: 90 });
card.busy = false;
if (card.queued !== null) { var q = card.queued; card.queued = null; show(card, q, true); }
}
})
.add(p.ftop, { rotateX: [0, -90], '--flc-shade': [0, 0.35], duration: 300, ease: 'in(2)' })
.add(p.fbot, { rotateX: [90, 0], '--flc-shade': [0.3, 0], duration: 480, ease: 'outBounce' });
}
function show(card, v, animate) {
if (v === card.value) return;
if (!animate) {
card.value = v;
['top', 'bot', 'ftop', 'fbot'].forEach(function (k) { card.parts[k].textContent = v; });
return;
}
if (card.busy) { card.queued = v; return; }
flip(card, v);
}
function setup(el) {
var d = el.dataset;
el.classList.add('flc');
el.style.setProperty('--flc-card', d.card || DEFAULTS.card);
el.style.setProperty('--flc-ink', d.ink || DEFAULTS.ink);
el.textContent = '';
var sr = make('span', 'flc-sr', el);
sr.setAttribute('role', 'timer');
var c = {
el: el, sr: sr, zone: d.timeZone, twelve: d['12'] !== undefined,
h: makeCard(el), m: makeCard(el),
s: d.seconds !== undefined ? makeCard(el, 'flc-sec') : null
};
[c.h, c.m, c.s].forEach(function (k) { if (k) k.el.setAttribute('aria-hidden', 'true'); });
if (c.twelve) c.ampm = make('b', 'flc-ampm', c.h.el, 'AM');
return c;
}
function render(c, animate, stagger) {
var t = now(c.zone);
var h = c.twelve ? (t.h % 12 || 12) : t.h;
var vals = [[c.h, two(h)], [c.m, two(t.m)]];
if (c.s) vals.push([c.s, two(t.s)]);
vals.forEach(function (v, i) {
if (stagger) setTimeout(function () { show(v[0], v[1], true); }, i * stagger);
else show(v[0], v[1], animate);
});
if (c.ampm) c.ampm.textContent = t.h < 12 ? 'AM' : 'PM';
c.sr.textContent = two(h) + ':' + two(t.m) + (c.twelve ? (t.h < 12 ? ' AM' : ' PM') : '');
}
function start(c, animate) {
// Arriving, the cards flip one after another from 00 to the time.
render(c, animate, animate ? 220 : 0);
(function tick() {
setTimeout(function () {
render(c, animate);
tick();
}, 1000 - (Date.now() % 1000) + 15);
})();
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
io.unobserve(e.target);
start(e.target._flc, true);
});
}, { threshold: 0.3 });
document.querySelectorAll('[data-flip-clock]').forEach(function (el) {
if (el._flc) return;
el._flc = setup(el);
// Reduced motion: the numbers simply change, without flipping.
if (reduced()) start(el._flc, false);
else io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-flip-clock style="font-size: 96px"></div>
<!-- 12-hour, with seconds, in Tokyo: -->
<div data-flip-clock data-12 data-seconds data-time-zone="Asia/Tokyo"></div>