← E-commerce

Barcode Scan

A red laser line sweeps up and down over a barcode inside a scanner’s viewfinder. When it reads, the frame flashes white, the laser and corners turn green, and a “Scanned” tag pops up. The barcode is a real EAN-13, drawn from the number you give it.

How to use it

  1. Paste the snippet just before </head>.
  2. Add an empty element with data-barcode-scan and the product number in data-code. Size it with font-size: it is 14em wide.

Options

Add any of these attributes to the same element.

data-code12 or 13 digits. It works out the check digit itself. Default 5012345678900.
data-passesHow many sweeps before it reads. Default 2.
data-labelText on the tag. Default “Scanned”.
data-colorLaser colour. Default #ef4444.
data-success-colorColour once it reads. Default #16a34a.
data-loopScan again every few seconds while in view.

Screen readers hear “Barcode” and the number. It fires a scan event with e.detail.code, and animacious.barcodeScan(element) scans it again.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Barcode Scan: a red laser line sweeps up and down over a barcode inside
    // a scanner's viewfinder. When it reads, the frame flashes, the laser and
    // corners turn green and a "Scanned" tag pops up.
    // Usage: <div data-barcode-scan data-code="5012345678900"></div>
    // Optional: data-code (12 or 13 digits; it draws a real EAN-13 barcode,
    // working out the check digit; default 5012345678900), data-passes (laser
    // sweeps before it reads, default 2), data-label (tag text, default
    // "Scanned"), data-color (laser colour, default #ef4444),
    // data-success-color (default #16a34a), data-loop (scan again every few
    // seconds while in view). Size it with font-size: it is 14em wide.
    // Fires a "scan" event with e.detail.code. Script use:
    // animacious.barcodeScan(el) scans it again.
    var DEFAULTS = { code: '5012345678900', passes: 2, label: 'Scanned', color: '#ef4444', success: '#16a34a', hold: 2000 };

    var css =
      '.bcs{position:relative;display:block;width:14em;max-width:100%;padding:1em 1.1em;box-sizing:border-box;--bcs-c:#ef4444}' +
      '.bcs-code{position:relative;border-radius:.4em;background:#fff;padding:.5em .4em .3em}' +
      '.bcs-code svg{display:block;width:100%;height:auto}' +
      '.bcs-corner{position:absolute;width:1.2em;height:1.2em;border:.22em solid #1a1a2e;transition:border-color .25s}' +
      '.bcs-corner:nth-child(1){left:0;top:0;border-right:0;border-bottom:0;border-top-left-radius:.5em}' +
      '.bcs-corner:nth-child(2){right:0;top:0;border-left:0;border-bottom:0;border-top-right-radius:.5em}' +
      '.bcs-corner:nth-child(3){left:0;bottom:0;border-right:0;border-top:0;border-bottom-left-radius:.5em}' +
      '.bcs-corner:nth-child(4){right:0;bottom:0;border-left:0;border-top:0;border-bottom-right-radius:.5em}' +
      '.bcs-ok .bcs-corner{border-color:var(--bcs-ok)}' +
      '.bcs-laser{position:absolute;left:.5em;right:.5em;top:0;height:2px;margin-top:-1px;border-radius:2px;background:var(--bcs-c);opacity:0;pointer-events:none;' +
      'box-shadow:0 0 .35em .1em var(--bcs-c),0 0 1.2em .2em var(--bcs-c)}' +
      '.bcs-laser::after{content:"";position:absolute;left:0;right:0;top:1px;height:1.4em;background:linear-gradient(var(--bcs-c),transparent);opacity:.18;transform-origin:top;transform:scaleY(var(--bcs-dir,1))}' +
      '.bcs-flash{position:absolute;inset:0;border-radius:.6em;background:#fff;opacity:0;pointer-events:none}' +
      '.bcs-tag{position:absolute;left:50%;bottom:-.75em;display:flex;align-items:center;gap:.3em;white-space:nowrap;padding:.25em .75em .25em .55em;border-radius:999px;' +
      'background:var(--bcs-ok);color:#fff;font-size:.8em;font-weight:700;box-shadow:0 .3em .8em rgba(15,15,30,.2);opacity:0;pointer-events:none}' +
      '.bcs-tag svg{width:1em;height:1em}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    // EAN-13 patterns: L-codes; G-codes are L reversed and inverted; R-codes are L inverted.
    var L = ['0001101', '0011001', '0010011', '0111101', '0100011', '0110001', '0101111', '0111011', '0110111', '0001011'];
    var PARITY = ['LLLLLL', 'LLGLGG', 'LLGGLG', 'LLGGGL', 'LGLLGG', 'LGGLLG', 'LGGGLL', 'LGLGLG', 'LGLGGL', 'LGGLGL'];
    function inv(p) { return p.replace(/./g, function (c) { return c === '1' ? '0' : '1'; }); }
    function rev(p) { return p.split('').reverse().join(''); }

    function digits(code) {
      var c = String(code).replace(/\D/g, '');
      if (c.length < 12) c = (c + '000000000000').slice(0, 12);
      c = c.slice(0, 12);
      var sum = 0;
      for (var i = 0; i < 12; i++) sum += +c[i] * (i % 2 ? 3 : 1);
      return c + ((10 - (sum % 10)) % 10);
    }

    function barcodeSvg(code) {
      var par = PARITY[+code[0]], bits = '101';
      for (var i = 1; i <= 6; i++) bits += par[i - 1] === 'L' ? L[+code[i]] : rev(inv(L[+code[i]]));
      bits += '01010';
      for (i = 7; i <= 12; i++) bits += inv(L[+code[i]]);
      bits += '101';
      var q = 9, out = '';
      for (i = 0; i < bits.length; i++) {
        if (bits[i] !== '1') continue;
        var j = i;
        while (bits[j + 1] === '1') j++;
        var guard = i < 3 || (i >= 45 && i < 50) || i >= 92;
        out += '<rect x="' + (q + i) + '" y="0" width="' + (j - i + 1) + '" height="' + (guard ? 62 : 56) + '"/>';
        i = j;
      }
      var t = '<text font-family="ui-monospace,Menlo,Consolas,monospace" font-size="8.5" text-anchor="middle" y="65" dominant-baseline="hanging">';
      var txt = t.replace('text-anchor="middle"', 'text-anchor="end"') + '<tspan x="' + (q - 1.5) + '">' + code[0] + '</tspan></text>';
      for (i = 1; i <= 12; i++) {
        var x = q + (i <= 6 ? 3 + (i - 1) * 7 + 3.5 : 50 + (i - 7) * 7 + 3.5);
        txt += t.replace('text-anchor="middle"', 'text-anchor="middle" x="' + x + '"') + code[i] + '</text>';
      }
      return '<svg viewBox="0 0 ' + (95 + 2 * q) + ' 74" fill="#1a1a2e" aria-hidden="true">' + out + txt + '</svg>';
    }

    var TICK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>';

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
    function on(v) { return v !== undefined && v !== 'false'; }

    function setup(el) {
      var d = el.dataset;
      var code = digits(d.code || DEFAULTS.code);
      el.classList.add('bcs');
      el.style.setProperty('--bcs-c', d.color || DEFAULTS.color);
      el.style.setProperty('--bcs-ok', d.successColor || DEFAULTS.success);
      el.setAttribute('role', 'img');
      el.setAttribute('aria-label', 'Barcode ' + code);
      el.innerHTML =
        '<span class="bcs-corner"></span><span class="bcs-corner"></span><span class="bcs-corner"></span><span class="bcs-corner"></span>' +
        '<div class="bcs-code">' + barcodeSvg(code) + '</div>' +
        '<span class="bcs-flash"></span><span class="bcs-laser"></span>' +
        '<span class="bcs-tag">' + TICK + '<span></span></span>';
      el.querySelector('.bcs-tag span').textContent = d.label || DEFAULTS.label;
      var s = el._bcs = {
        code: code, box: el.querySelector('.bcs-code'), laser: el.querySelector('.bcs-laser'), flash: el.querySelector('.bcs-flash'),
        tag: el.querySelector('.bcs-tag'), corners: el.querySelectorAll('.bcs-corner'), busy: false, visible: false
      };
      var place = function () { s.tag.style.marginLeft = -s.tag.offsetWidth / 2 + 'px'; };
      place();
      new ResizeObserver(place).observe(s.tag);
      if (reduced()) {
        el.classList.add('bcs-ok');
        anime.utils.set(s.tag, { opacity: 1 });
        return;
      }
      new IntersectionObserver(function (entries) {
        s.visible = entries[0].isIntersecting;
        if (s.visible && !s.busy && (!s.played || s.waiting)) { s.waiting = false; scan(el); }
      }, { threshold: 0.3 }).observe(el);
    }

    function scan(el) {
      var s = el._bcs, d = el.dataset;
      if (s.busy) return;
      s.busy = true;
      s.played = true;
      el.classList.remove('bcs-ok');
      s.laser.style.background = '';
      el.style.setProperty('--bcs-c', d.color || DEFAULTS.color);
      anime.utils.set(s.tag, { opacity: 0 });
      anime.utils.set(s.laser, { scaleX: 1 });
      // The laser runs over the bars only, not the digits.
      var top = s.box.offsetTop + s.box.offsetHeight * 0.12, bottom = s.box.offsetTop + s.box.offsetHeight * 0.7;
      var passes = Math.max(1, +d.passes || DEFAULTS.passes);
      var tl = anime.createTimeline({
        onComplete: function () {
          s.busy = false;
          el.dispatchEvent(new CustomEvent('scan', { bubbles: true, detail: { code: s.code } }));
          if (on(d.loop)) setTimeout(function () {
            if (s.visible) scan(el); else s.waiting = true;
          }, DEFAULTS.hold);
        }
      });
      tl.add(s.laser, { opacity: [0, 1], y: top, duration: 200 }, 0);
      var t = 200;
      for (var i = 0; i < passes; i++) {
        (function (down) {
          tl.call(function () { s.laser.style.setProperty('--bcs-dir', down ? -1 : 1); }, t);
        })(i % 2 === 0);
        tl.add(s.laser, { y: i % 2 ? [bottom, top] : [top, bottom], duration: 700, ease: 'inOut(2)' }, t);
        t += 700;
      }
      // Read: settle in the middle, flash, go green.
      var mid = (top + bottom) / 2, ok = d.successColor || DEFAULTS.success;
      tl.add(s.laser, { y: mid, duration: 260, ease: 'out(2)' }, t)
        .call(function () {
          el.classList.add('bcs-ok');
          el.style.setProperty('--bcs-c', ok);
        }, t + 260)
        .add(s.flash, { opacity: [0.85, 0], duration: 450, ease: 'out(2)' }, t + 260)
        .add(s.corners, { scale: [1, 1.15, 1], duration: 420, ease: 'out(2)' }, t + 260)
        .add(s.tag, { opacity: [0, 1], y: ['.8em', '0em'], scale: [0.6, 1], duration: 500, ease: 'outBack(2)' }, t + 380)
        .add(s.laser, { opacity: 0, scaleX: [1, 0.6], duration: 400, ease: 'in(2)' }, t + 700);
    }

    window.animacious = window.animacious || {};
    window.animacious.barcodeScan = function (el) { if (el && el._bcs) scan(el); };

    function init() {
      document.querySelectorAll('[data-barcode-scan]').forEach(function (el) {
        if (el._bcs) return;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-barcode-scan data-code="5012345678900"></div>