# Read QR codes from a webcam in the browser

> Use the built-in BarcodeDetector API where it exists (Chromium browsers), and fall back to zxing-wasm elsewhere: Firefox and Safari do not ship BarcodeDetector. Open the camera with getUserMedia using facingMode environment, then decode frames in a requestAnimationFrame loop. Everything runs locally; no frame ever needs to leave the device.

Source: https://useqr.app/docs/developers/read-qr-codes-from-a-webcam-in-the-browser · Last reviewed 2026-08-21 · UseQR is free forever, no signup.

---

## Two decoders, one interface

The platform has a native decoder (`BarcodeDetector`), but only in Chromium-based
browsers (Chrome and Edge on Android, ChromeOS and macOS, per current compatibility
data). Firefox and Safari do not ship it, so a real implementation is always
feature-detect plus fallback. The good news: zxing-wasm exposes a nearly identical
detect-from-bitmap call, so the fallback is a few lines, not a rewrite.

```js
async function makeDetector() {
  if ("BarcodeDetector" in window) {
    const formats = await BarcodeDetector.getSupportedFormats();
    if (formats.includes("qr_code")) {
      const native = new BarcodeDetector({ formats: ["qr_code"] });
      return (source) => native.detect(source);
    }
  }
  const { readBarcodes } = await import("zxing-wasm/reader");
  return async (source) => {
    const bitmap = await createImageBitmap(source);
    const results = await readBarcodes(bitmap, { formats: ["QRCode"] });
    return results.map((r) => ({ rawValue: r.text }));
  };
}
```

Note the double check: some browsers expose the constructor but not the `qr_code`
format, so query `getSupportedFormats()` rather than trusting `in window`.

## The camera and the scan loop

```js
const video = document.querySelector("video");
const stream = await navigator.mediaDevices.getUserMedia({
  video: { facingMode: "environment", width: { ideal: 1280 } },
});
video.srcObject = stream;
await video.play();

const detect = await makeDetector();
async function tick() {
  const codes = await detect(video);
  if (codes.length > 0) {
    handle(codes[0].rawValue);   // decoded text
    stream.getTracks().forEach((t) => t.stop());
    return;
  }
  requestAnimationFrame(tick);
}
tick();
```

Three constraint choices that matter:

- `facingMode: "environment"` selects the rear camera on phones, the front camera
  mirrors the image and sits at the wrong distance for scanning.
- `width: { ideal: 1280 }` is enough: a QR module needs only a few pixels to decode,
  and 4K frames just slow the loop down. See
  [module size vs camera resolution](/docs/spec/module-size-vs-camera-resolution).
- Decode at most once per animation frame. Running the detector on every frame of a
  60 fps stream wastes battery for no extra hits; every second or third frame is fine.

## Torch, focus, permissions

On Android Chrome you can often light the scene: check
`track.getCapabilities().torch` and, if true,
`track.applyConstraints({ advanced: [{ torch: true }] })`. iOS Safari does not expose
the torch to web pages. Continuous autofocus is the default on phone cameras; there is
no reliable cross-browser way to force focus, so if scanning fails, moving the code to
**15–30 cm** from the lens helps more than any constraint.

`getUserMedia` requires a secure context (HTTPS or `localhost`), and a permission
prompt. Handle denial gracefully: offer a file-input fallback
(`<input type="file" accept="image/*" capture>`) and decode the chosen photo through
the same detector function, since both accept image sources.

## Privacy is the point

This entire pipeline runs on-device. No frame is uploaded, which matters when the code
in front of the camera is a [WiFi password](/wifi-qr-code), a
[payment code](/docs/payments/upi-qr-code-format) or a vCard. UseQR's own
[scanner](/scan) is built exactly this way (camera and image decoding, fully
client-side), and the same argument applies to generation:
[client-side vs server-side](/docs/security/client-side-vs-server-side-qr-generation).
If you need to decode a stored image server-side instead, that is a
[different pattern](/docs/developers/decode-a-qr-code-programmatically).

## FAQ

### How do I scan a QR code with JavaScript in the browser?
Feature-detect BarcodeDetector, fall back to zxing-wasm, open the rear camera with getUserMedia and call the detector on the video element in a requestAnimationFrame loop. The whole implementation is around 30 lines.

### Which browsers support the BarcodeDetector API?
Chromium-based browsers: Chrome and Edge on Android, ChromeOS and macOS. Firefox and Safari do not support it, so production code always needs a WebAssembly fallback such as zxing-wasm.

### Does browser QR scanning upload the camera feed?
No. Both BarcodeDetector and zxing-wasm decode frames locally on the device. No image leaves the browser unless your code explicitly uploads one, which makes this approach suitable for sensitive payloads.

### Why does my webcam QR scanner fail to focus?
Laptop webcams are fixed-focus and phone cameras autofocus continuously; neither can be forced reliably from the web. Hold the code 15–30 cm from the lens, fill a quarter of the frame, and make sure the quiet zone is visible.

## Try it

- https://useqr.app/scan
- https://useqr.app/url
- https://useqr.app/validate
