• Tutorials
  • Building a QR Code & NFC Tag Scanner with Vanilla JS and Vite

    Ansichten54

    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 qrbox option draws a targeting overlay so the user knows where to aim.
    • Camera access requires HTTPS (or localhost). Serving over plain HTTP will cause getUserMedia to 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:

    1. NFC not supported — if 'NDEFReader' in window is false, the "Start NFC Scan" button is hidden and a clear message is shown explaining the requirement.
    2. Camera error — if getUserMedia fails (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
    profile image of Petar Vasilev

    Petar Vasilev

    Petar is a web developer at Mitkov Systems GmbH. He is fascinated with the web. Works with Laravel and its ecosystem. Loves learning new stuff.

    More posts from Petar Vasilev