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
- Paste the snippet just before
</head>. - Add an empty element with
data-barcode-scanand the product number indata-code. Size it withfont-size: it is 14em wide.
Options
Add any of these attributes to the same element.
data-code | 12 or 13 digits. It works out the check digit itself. Default 5012345678900. |
data-passes | How many sweeps before it reads. Default 2. |
data-label | Text on the tag. Default “Scanned”. |
data-color | Laser colour. Default #ef4444. |
data-success-color | Colour once it reads. Default #16a34a. |
data-loop | Scan 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.
<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><div data-barcode-scan data-code="5012345678900"></div>