Modern browsers expose powerful hardware APIs that make it possible to build native-feeling scanner experiences entirely in the browser — no app store, no installation, no native code. This project combines two of those APIs into a single lightweight tool: a QR code scanner powered by the device camera, and an NFC tag reader using the Web NFC API.
#Live Demo
#What It Does
The app has two modes, selectable via a tab:
- QR Code — starts the rear camera and continuously scans for QR codes. Once a code is detected, the camera stops and the result is displayed. If the result is a URL, a direct "Open URL" link appears alongside a copy button.
- NFC Tag — uses the Web NFC API to listen for nearby NFC tags. When a tag is tapped, all NDEF records are decoded and displayed. URLs inside records are rendered as clickable links.
#Tech Stack
| Concern | Tool |
|---|---|
| Bundler | Vite |
| QR scanning | html5-qrcode |
| NFC scanning | Web NFC API (`NDEFReader`) |
| Languages | HTML, CSS, vanilla JavaScript (ES modules) |
No frameworks, no UI libraries — just a single main.js entry point and a stylesheet.
#QR Code Scanning
The QR scanner is built on top of the html5-qrcode library, which wraps the browser's getUserMedia API and runs a QR decoder on each video frame.
1import { Html5Qrcode } from 'html5-qrcode';
2
3const qrScanner = new Html5Qrcode('qr-reader');
4
5await qrScanner.start(
6 { facingMode: 'environment' }, // rear camera
7 { fps: 15, qrbox: { width: 250, height: 250 } },
8 (decodedText) => handleResult(decodedText),
9 () => {} // ignore per-frame errors
10);
A few things worth noting:
facingMode: 'environment'selects the rear camera on mobile, which is what you want for scanning.- The
qrboxoption draws a targeting overlay so the user knows where to aim. - Camera access requires HTTPS (or
localhost). Serving over plain HTTP will causegetUserMediato be blocked by the browser.
#NFC Scanning
The Web NFC API is only available in Chrome on Android (as of 2026). It requires a user gesture to start and triggers a permission prompt on first use.
1const reader = new NDEFReader();
2const abort = new AbortController();
3
4await reader.scan({ signal: abort.signal });
5
6reader.addEventListener('reading', ({ message, serialNumber }) => {
7 message.records.forEach(record => {
8 // decode each NDEF record
9 });
10});
NDEF messages can contain multiple records of different types (text, url, mime, smart-poster). Each record's binary data buffer is decoded with TextDecoder:
1function decodeRecord(record) {
2 const decoder = new TextDecoder(record.encoding || 'utf-8');
3 return decoder.decode(record.data);
4}
An AbortController is used to stop the scan cleanly when the user clicks "Stop Scanning", which avoids leaving a dangling listener.
#Graceful Degradation
Not every browser supports every API. The app handles this at two levels:
- NFC not supported — if
'NDEFReader' in windowis false, the "Start NFC Scan" button is hidden and a clear message is shown explaining the requirement. - Camera error — if
getUserMediafails (permission denied, no camera, HTTP context), the error is caught and surfaced to the user.
#Project Setup
1npm install
2npm run dev # development server with HMR
3npm run build # production build → dist/
Vite handles the html5-qrcode CommonJS-to-ESM transform automatically via its dependency pre-bundling step — no extra configuration needed.
#Browser Support
| Feature | Requirement |
|---|---|
| QR scanning | Any modern browser with camera access over HTTPS |
| NFC scanning | Chrome 89+ on Android, NFC hardware required |