Point a camera at an animated QR. Get the bytes.
A single QR caps out around 3 KB. Anything larger
is split into an animated, fountain-coded sequence a
camera can rebuild even while missing frames. Senders are everywhere;
the browser lacked a good receiving end. This is that
end: a pure decode core, pluggable frame sources, and a drop-in <ur-scanner> element.
$ npm i @nkwib/ur-scanner @ngraveio/bc-urOne QR is ~3 KB
QR is a zero-infrastructure transport: no pairing, no network, works across an air gap and in a power cut. But a single code is tiny, so bigger payloads must animate.
Fountain-coded, loss-tolerant
BC-UR fountain-codes the sequence: the receiver rebuilds the whole payload from any set of frames a bit larger than the original, and can join the stream at any point. Miss frame 7? It doesn't matter.
The receiving end
Every hardware wallet ships a sender; the web had no good, framework-agnostic receiver. This is not a wallet library, it hands you bytes. Wallet payloads are one recipe among many.
Drop-in web component
Import the browser-only subpath, add the tag, listen for ur-complete. Ten lines, any framework, or none.
Prefer to own the loop? The headless URReceiver core takes strings from a camera, a file, or a test fixture and is safe to import in SSR bundles.
<ur-scanner auto-start expected-type="bytes"></ur-scanner>
<script type="module">
import '@nkwib/ur-scanner/element';
const scanner = document.querySelector('ur-scanner');
scanner.addEventListener('ur-complete', (e) => {
// e.detail.cbor is a Uint8Array; payloads are just bytes.
console.log(`received ${e.detail.cbor.length} bytes`);
scanner.stop();
});
</script>All processing is local. No telemetry.
No frame, byte, or pixel ever leaves the page. There is no network call
of any kind. The camera stream goes to a canvas, the canvas to a QR
detector, the strings to a decoder, all inside your tab. The library
opens the camera only when you call start() and releases it
on stop(). Even the fonts on this page are self-hosted.
Browser support
The core decode logic runs anywhere JavaScript does. Camera and detection depend on the browser. Native BarcodeDetector is fastest; where it's missing, install jsqr and it loads lazily.
| Browser | QR detection | Verified |
|---|---|---|
| Chrome / Edge (desktop + Android) | native BarcodeDetector | yes (Playwright/Chromium) |
| Safari (macOS 14+, iOS 17+) | jsqr fallback | help wanted |
| Firefox | jsqr fallback | help wanted |
The honest, per-device matrix (a community-PR target) lives in the compatibility docs.
See the bytes transfer
Open the demo on a laptop and your phone, point one at the other, and watch the ring fill. No second device? Fixture mode runs the whole pipeline on one screen.
The benchmark page measures decode attempts per second and time to complete on your own phone, through your own lens. It is the number no headless run can give you.