JavaScript Barcode Detector Table
| Piece | What it does | Field note |
|---|---|---|
BarcodeDetector.getSupportedFormats() | The platform check | the feature detect: empty or thrown means no native decoder - branch to your library fallback there and never again |
new BarcodeDetector({formats}) | The scoped decoder | formats is hint and filter at once: qr_code only skips a dozen decode passes, roughly doubling scan rate on busy frames |
detector.detect(source) | The one-call scan | accepts img, video, canvas, Blob, ImageData; returns decoded values - the whole file-picker scan feature is this one await |
result.rawValue + format | The payload | rawValue is attacker-controlled input: show it before opening, verify signatures in business flows, never auto-navigate |
result.cornerPoints | The geometry | four corners in source pixels - scale by display ratio and you have the targeting overlay for free |
the live loop | The camera scan | setTimeout at 10fps over a playing getUserMedia video beats rAF-per-frame on battery; stop on hidden and on first hit |
the dedupe cooldown | The noise gate | same rawValue within 2 seconds is noise, not a scan - the classic bug is a scanner that keeps vibrating after success |
the support truth | The matrix | Chromium-only today (Shape Detection family); no Safari/Firefox - wrap native and WASM decoders in one results interface |
The Barcode Detection API turns QR and barcode scanning from a JavaScript marathon into a native call: hand the detector an image source - an img, a canvas, a video frame, a Blob, ImageData - and it returns the decoded contents with bounding boxes and corner points. No library, no WASM bundle, no ZXing port: the platform decoder does the work, which is why it is fast and why it is unevenly distributed.
Bottom line: the flow is three lines - const detector = new BarcodeDetector({formats: ['qr_code']}); const results = await detector.detect(imageSource); - and each result carries rawValue, format, and cornerPoints (the four corners, in image coordinates, which is everything you need to draw a targeting overlay). The formats array is a performance hint and a filter at once: restricting to qr_code skips decoding passes for the dozen other symbologies, which roughly doubles scan rate on busy frames.
The honest part: this is a Chromium-platform API today (Chrome, Edge, Android browsers via the Shape Detection family) with no Safari or Firefox implementation, so a production scanner needs a fallback library behind the feature detect - and the fallback choice (a JS/WASM decoder) is exactly what the native path lets you skip where it exists. Detect once with BarcodeDetector.getSupportedFormats(), branch once at startup, and keep the rest of your scanner (camera loop, overlay, UX) identical across both paths.
How to use
- Check the platform first: const formats = await BarcodeDetector.getSupportedFormats(); - an empty or thrown result means no native decoder; branch to your library fallback there and never again.
- Construct with intent: const detector = new BarcodeDetector({formats: ['qr_code', 'ean_13']}); - only the symbologies you actually accept; a bare constructor detects everything and pays for it per frame.
- Scan any static source: const codes = await detector.detect(imgOrCanvasOrBlobOrImageData); - works on anything paintable; for a file-picker flow (scan a photo) this is the whole feature: one call, draw cornerPoints on a canvas overlay.
- Build the live loop: requestAnimationFrame or setTimeout over video frames - grab the video element (playing, with getUserMedia camera behind it) and call detector.detect(video) each tick; the API reads the current frame, nothing to draw first.
- Use the geometry: results.forEach(r => drawBox(r.cornerPoints, r.rawValue)) - cornerPoints are in source-image pixels, so scale by the display/canvas ratio before overlaying; rawValue is the decoded string, format the symbology, and remember QR results may be URLs the user should see before opening.
Frequently asked questions
Which image sources work with detect(), and which fail silently?
The spec accepts anything ImageBitmapSource: HTMLImageElement, SVG image, HTMLVideoElement, HTMLCanvasElement, OffscreenCanvas, ImageBitmap, ImageData, Blob, and plain ImageData-shaped objects. In practice two traps dominate. First, video: the video must be actively playing with real frames - detect() on a paused or metadata-only video returns an empty array without error, which reads as the scanner being broken when it is actually the source. Second, cross-origin taint: an img or canvas that drew cross-origin content without CORS taints the source, and detection on tainted sources throws or returns empty depending on engine - the same rule that blocks getImageData, so the fix is the same too: crossorigin attributes and proper CORS headers on image hosts, or same-origin/Blob URLs. Blob sources are the safe universal choice for file uploads: decodeImage to a Blob via canvas.toBlob and you sidestep taint entirely. And note orientation: EXIF-rotated JPEGs from phone cameras may decode rotated in some engines - if photos fail to scan, redraw the image to a canvas with correct orientation and scan that.
How do I build a live camera scanner that does not melt the battery?
Three budgets decide it: frame rate, resolution, and detection scope. Frame rate: the detector is fast (often under 10ms per frame on modern phones) but not free - a 10fps setTimeout loop outperforms rAF-per-frame for battery with no user-visible difference, because barcodes do not move fast. Resolution: request the camera at 1280x720 with facingMode: 'environment' - 4K frames slow both the copy and the decode, and QR tolerates modest resolution fine; zoom in optically (digital zoom crops the frame) only when codes are small. Scope: pass formats in the constructor so each frame skips a dozen decoder passes, and stop the loop entirely when the tab is hidden (visibilitychange) and when a code is found - the classic bug is a scanner that keeps scanning after success, vibrating on every duplicate. Deduplicate by rawValue with a cooldown: same value within 2 seconds is noise, not a new scan.
What do I do when the API is missing - Safari, Firefox, older Chrome?
Branch once at startup and keep the whole scanner stack identical above the decoder. The feature detect is await BarcodeDetector.getSupportedFormats() inside try/catch (constructing without the guard throws on unsupported engines); on failure, load your fallback decoder - a WASM port of ZXing or similar - lazily, only when the scanner view opens, so the library cost is paid by scanner users only. The seam that makes this clean: your scanner component speaks in image sources and expects results shaped {rawValue, format, cornerPoints}; wrap the native detector and the library in that same interface, and the camera loop, overlay drawing, dedupe and UX never learn which decoder ran. Two platform notes from the field: iOS Safari needs the fallback today, and fallback decoders are slower per frame - lower the loop rate and prefer higher-resolution single-shot scans (take a frame, decode, retry) over continuous video scanning when the fallback path is active.
Is this secure? What are the real-world gotchas with scanned content?
The API itself is safe - it reads images you already have and grants no camera access by itself (the camera comes from getUserMedia with its usual permission prompt) - but the content is attacker-controlled input like any other. QR codes are a delivery mechanism for URLs, and auto-navigating on a successful scan is the classic phishing funnel: show the decoded value, require a tap to open, and warn on non-http schemes (javascript: does not navigate from location assignment, but custom schemes and shorteners obfuscate destinations). Validate length - a 4KB QR payload is not a URL, it is a payload smuggling attempt for your text field. And when scanning for business flows (tickets, pairing codes), verify a signature or nonce server-side: rawValue is trivially forgeable by anyone who can print a code. One engine quirk worth knowing: some Android OEM builds return duplicates across frames even within the cooldown - dedupe on your side, never trust the detector to be stateless-lazy in your favor.