← Numbers

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

  1. Paste the snippet just before </head>.
  2. Add data-binary-counter to an empty element to count up for ever, or data-binary-counter="42" to count up to 42 and stop. Size it with font-size; each light is 1.9em wide.

Options

Add any of these attributes to the same element.

data-bitsHow many lights. Default 8 (0–255).
data-fromWhere the count starts. Default 0.
data-intervalTime per count. Default 700 ms.
data-durationWith a target: the longest the count-up may take, so big numbers count faster. Default 5000 ms.
data-loopWith a target: start again after a pause.
data-places="false"Hide the place values (128, 64…) under the lights.
data-hide-valueNo decimal number underneath.
data-colorColour of a lit bit. Default #6a0dad.
1 · Snippet
<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>
2 · Your HTML
<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>