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 @blocco/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 '@blocco/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.