Binary Counter
A row of lights counts in binary. Each bit is a tile that flips over to light up as a 1 or go dark as a 0, and the carry ripples in from the right. The place values sit underneath and the number is written in decimal below.
How to use it
- Paste the snippet just before
</head>. - Add
data-binary-counterto an empty element to count up for ever, ordata-binary-counter="42"to count up to 42 and stop. Size it withfont-size; each light is 1.9em wide.
Options
Add any of these attributes to the same element.
data-bits | How many lights. Default 8 (0–255). |
data-from | Where the count starts. Default 0. |
data-interval | Time per count. Default 700 ms. |
data-duration | With a target: the longest the count-up may take, so big numbers count faster. Default 5000 ms. |
data-loop | With a target: start again after a pause. |
data-places="false" | Hide the place values (128, 64…) under the lights. |
data-hide-value | No decimal number underneath. |
data-color | Colour of a lit bit. Default #6a0dad. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Binary Counter: a row of lights counts in binary. Each bit is a tile that
// flips over to light up as a 1 or go dark as a 0, the carry rippling from
// the right, with the place values underneath and the number in decimal.
// Usage: <div data-binary-counter></div> (counts up for ever, wrapping)
// <div data-binary-counter="42"></div> (counts up to 42 and stops)
// Optional: data-bits (how many lights, default 8), data-from (where it
// starts, default 0), data-interval (ms per count, default 700),
// data-duration (with a target: the most the count-up may take, default
// 5000 ms), data-loop (with a target: start again after a pause),
// data-places="false" (hide the place values), data-hide-value (no decimal
// readout), data-color (lit colour, default #6a0dad).
// Size it with font-size: each light is 1.9em wide.
var DEFAULTS = { bits: 8, from: 0, interval: 700, duration: 5000, hold: 2400, color: '#6a0dad' };
var css =
'.bnr{display:inline-flex;flex-direction:column;align-items:center;gap:.6em;font-variant-numeric:tabular-nums;max-width:100%}' +
'.bnr-row{display:flex;gap:.35em}' +
'.bnr-bit{display:flex;flex-direction:column;align-items:center;gap:.35em}' +
'.bnr-tile{position:relative;width:1.9em;height:2.3em;perspective:6em}' +
'.bnr-card{position:absolute;inset:0;transform-style:preserve-3d}' +
'.bnr-face{position:absolute;inset:0;display:grid;place-items:center;border-radius:.35em;backface-visibility:hidden;font-weight:800;font-size:1.25em;line-height:1}' +
'.bnr-off{background:rgba(127,127,140,.12);color:rgba(127,127,140,.75);box-shadow:inset 0 0 0 .06em rgba(127,127,140,.22)}' +
'.bnr-on{transform:rotateX(180deg);background:var(--bnr-c);color:#fff;box-shadow:0 0 .7em var(--bnr-c),inset 0 .12em .2em rgba(255,255,255,.35),inset 0 -.12em .2em rgba(0,0,0,.2)}' +
'.bnr-place{font-size:.62em;font-weight:600;opacity:.55}' +
'.bnr-dec{font-size:1.5em;font-weight:800;letter-spacing:-.02em;line-height:1}' +
'.bnr-dec small{font-size:.5em;font-weight:600;opacity:.55;margin-right:.35em;letter-spacing:.06em}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function setup(el) {
var d = el.dataset, attr = el.getAttribute('data-binary-counter');
var B = {
el: el, bits: Math.max(1, Math.min(16, parseInt(d.bits, 10) || DEFAULTS.bits)), from: +d.from || DEFAULTS.from,
target: attr !== '' && attr !== null && !isNaN(+attr) ? +attr : null, interval: +d.interval || DEFAULTS.interval,
duration: +d.duration || DEFAULTS.duration, loop: d.loop !== undefined && d.loop !== 'false',
still: reduced(), visible: false, timer: 0, value: -1, cards: []
};
B.mod = Math.pow(2, B.bits);
if (B.target !== null) {
B.target = Math.max(0, Math.min(B.mod - 1, Math.round(B.target)));
B.interval = Math.min(B.interval, Math.max(60, B.duration / Math.max(1, B.target - B.from)));
}
el.classList.add('bnr');
el.style.setProperty('--bnr-c', d.color || DEFAULTS.color);
var row = '';
for (var i = B.bits - 1; i >= 0; i--) {
row += '<span class="bnr-bit"><span class="bnr-tile"><span class="bnr-card"><span class="bnr-face bnr-off">0</span><span class="bnr-face bnr-on">1</span></span></span>' +
(d.places === 'false' ? '' : '<span class="bnr-place">' + Math.pow(2, i) + '</span>') + '</span>';
}
el.innerHTML = '<span class="bnr-row" aria-hidden="true">' + row + '</span>' +
(d.hideValue !== undefined ? '' : '<span class="bnr-dec" aria-hidden="true"><small>DEC</small><span></span></span>');
B.cards = [].slice.call(el.querySelectorAll('.bnr-card')).reverse(); // cards[0] is the lowest bit
B.dec = el.querySelector('.bnr-dec span');
el.setAttribute('role', 'img');
show(B, B.still && B.target !== null ? B.target : B.from, false);
return B;
}
// Flips the tiles that changed, lowest bit first so the carry ripples left.
function show(B, v, animate) {
v = ((v % B.mod) + B.mod) % B.mod;
var old = B.value, k = 0;
B.value = v;
for (var i = 0; i < B.bits; i++) {
var on = (v >> i) & 1, was = old < 0 ? -1 : (old >> i) & 1;
if (on === was) continue;
if (animate) {
anime.animate(B.cards[i], { rotateX: on ? 180 : 0, duration: 420, delay: k * 55, ease: 'out(3)' });
k++;
} else anime.utils.set(B.cards[i], { rotateX: on ? 180 : 0 });
}
if (B.dec) B.dec.textContent = v;
B.el.setAttribute('aria-label', 'Binary ' + v.toString(2).padStart(B.bits, '0') + ' = ' + v);
}
function tick(B) {
if (!B.visible) return;
if (B.target !== null && B.value >= B.target) {
if (B.loop) B.timer = setTimeout(function () { show(B, B.from, true); B.timer = setTimeout(function () { tick(B); }, 900); }, DEFAULTS.hold);
return;
}
show(B, B.value + 1, true);
B.timer = setTimeout(function () { tick(B); }, B.interval);
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var B = e.target._bnr;
B.visible = e.isIntersecting;
clearTimeout(B.timer);
if (B.visible) B.timer = setTimeout(function () { tick(B); }, 400);
});
}, { threshold: 0.25 });
document.querySelectorAll('[data-binary-counter]').forEach(function (el) {
if (el._bnr) return;
var B = (el._bnr = setup(el));
if (!B.still) io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-binary-counter></div>
<!-- six bits counting up to 42, in green: -->
<div data-binary-counter="42" data-bits="6" data-loop data-color="#16a34a" style="font-size: 20px"></div>