A mobile-first web app for bulk-scanning Code 128 barcodes with your phone camera, then pasting the results straight into Excel or Google Sheets.
Point the camera at a barcode, keep scanning, then tap Copy list — you get one value per line, ready to paste as rows.
- Fast decoding — ZXing-C++ compiled to WebAssembly, with the native
BarcodeDetectorAPI used automatically where the browser supports it. - Frame-synced scanning — decoding is driven by
requestVideoFrameCallback, so it tracks the camera's actual frame rate instead of polling on a timer. - Any orientation — horizontal, vertical, or upside-down barcodes all decode.
- Camera picker — defaults to the rear camera, switchable to any other camera on the device.
- Format validation — only values matching the expected pattern are kept; misreads are discarded.
- Misread protection — a code must decode identically twice before it is accepted.
- Automatic de-duplication — scanning the same barcode repeatedly adds it only once.
- One-tap copy — newline-separated output, one row per value when pasted into a spreadsheet.
- Build stamp — the running build ID is shown in the footer so you can confirm what's deployed.
docker build -t barcode-scanner .
docker run -d --name barcode-scanner -p 8080:80 -p 8444:443 barcode-scannerThen open:
| URL | Use |
|---|---|
https://localhost:8444 |
Local testing (accept the self-signed certificate warning) |
http://<host-ip>:8080 |
Plain HTTP origin, intended for a reverse proxy or tunnel |
Important
Browsers only grant camera access in a secure context. That means https://, or localhost. Plain HTTP over an IP address will not prompt for the camera — put a proxy with a valid certificate in front of port 80, or use the built-in HTTPS on port 443.
A self-signed certificate is generated on first container start, so the HTTPS port works out of the box.
Point your proxy at the container's HTTP port and let it terminate TLS. For example, with a Cloudflare Tunnel public hostname:
service: http://<host-ip>:8080Don't point the tunnel at the HTTPS port — the self-signed certificate will fail origin validation and return a 502.
npm install
npm run buildThe build writes dist/, containing the bundled JS, the HTML/CSS, and the ZXing .wasm binary. Serve that directory over HTTPS (or localhost) to test the camera.
Scanned values are validated against a regular expression in app.js. By default it accepts exactly 24 uppercase hexadecimal characters, e.g. 340966364CB0000005B142DE:
const VALID_CODE = /^[0-9A-F]{24}$/;Change that pattern to match your own codes, or relax it to /.+/ to accept anything.
Other tuning knobs live alongside it:
| Constant | Purpose |
|---|---|
READER_OPTIONS.minLineCount |
Matching scan lines required per decode. Higher rejects more motion blur. |
REQUIRED_READS |
How many times a code must decode identically before it is accepted. |
MAX_DECODE_WIDTH |
Frames are downscaled to this width before decoding. |
The camera stream is rendered with object-fit: cover, and each frame is cropped to exactly the region visible on screen before being handed to the decoder — so the on-screen guide, the preview, and the scanned area are always the same thing.
Decoding runs on whichever backend is available:
- The browser's native
BarcodeDetector, when it supportscode_128(Chrome on Android). - Otherwise
zxing-wasm, which covers iOS Safari and Firefox, whereBarcodeDetectoris unavailable.
Only one decode is ever in flight, so a slow frame can't queue up backlog.
| File | Purpose |
|---|---|
index.html |
Markup |
app.js |
Camera handling, decoding, validation, clipboard |
styles.css |
Mobile-first styling |
build.mjs |
Bundles the app and stamps the build ID |
Dockerfile |
Multi-stage build onto nginx |
nginx.conf |
HTTP + HTTPS servers and cache headers |
docker-entrypoint.sh |
Generates the self-signed certificate at startup |
Requires a browser with getUserMedia and WebAssembly — Safari 15.4+, Chrome 83+, Firefox 132+. BarcodeDetector is used when present but is not required.