Scanbot SDK has been acquired by Apryse! Learn more

Learn more
Skip to content

How to build a JavaScript QR code scanner with jsQR and qr-scanner

Kevin August 27, 2025 15 mins read
JavaScript QR code scanner tutorial

If you want to scan QR codes from your browser, you have several options, including zxing-js, html5-qrcode, and the browser-native barcode detection API.

Another popular choice is jsQR. Although it’s no longer being actively maintained, it still works well for basic use cases.

In this tutorial, you’ll learn how to build a simple QR code scanner in two different ways:

  1. Using the jsQR library directly.
  2. Using qr-scanner, which is a wrapper around jsQR with some convenient features.
Scanning a QR code with the jsQR library
Scanning a QR code with the jsQR library
Scanning a QR code with the qr-scanner library
Scanning a QR code with the qr-scanner library

Approach A: Using the jsQR library

First, we’ll create a QR code scanner web app with jsQR by following these steps:

  1. Defining the HTML structure
  2. Adding basic CSS styling
  3. Implementing the scanner logic in JavaScript

Let’s get started!

Step 1: Define the HTML structure

In your project folder, create your index.html and paste the following code:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>jsQR Demo</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <h1>jsQR Demo</h1>

  <video id="video" autoplay playsinline></video>
  <canvas id="canvas" hidden></canvas>

  <div>
    <button id="startBtn">Start Camera</button>
    <button id="stopBtn" disabled>Stop Camera</button>
    <button id="restartBtn" disabled>Restart Camera</button>
  </div>

  <div>
    <input type="file" id="fileInput" accept="image/*">
  </div>

  <div id="result">Scan a QR code...</div>

  <script src="https://cdn.jsdelivr.net/npm/jsqr/dist/jsQR.js"></script>
  <script src="app.js"></script>
</body>
</html>

Let’s take a closer look at the code:

  • <video> displays the live camera feed.
  • <canvas> captures each frame from the video and analyzes it for QR codes.
  • There are buttons for starting, stopping, and restarting the camera and uploading a file.
  • jsQR is loaded from a CDN before the code from app.js is run.

Step 2: Add basic CSS styling

Now create a style.css file with the following contents:

body {
  font-family: Arial, sans-serif;
  text-align: center;
  background: #f4f4f4;
  margin: 0;
  padding: 20px;
}

video, canvas {
  max-width: 100%;
  width: 400px;
  height: auto;
  margin: 10px 0;
  border: 2px solid #ccc;
  border-radius: 8px;
}

#result {
  margin-top: 20px;
  padding: 10px;
  font-size: 18px;
  font-weight: bold;
  color: black;
  background: #eee;
  border-radius: 8px;
}

button {
  margin: 10px;
  padding: 10px 20px;
  font-size: 16px;
  border: none;
  border-radius: 8px;
  background: #007bff;
  color: white;
  cursor: pointer;
}

button:disabled {
  background: #999;
  cursor: not-allowed;
}

.flash {
  animation: flash-bg 0.8s ease;
}

@keyframes flash-bg {
  0% { background: yellow; }
  100% { background: #eee; }
}

To visualize when a QR code has been detected, we’ll let the result box flash yellow briefly when a QR is detected (.flash).

Step 3: Implement the scanner logic in JavaScript

Finally, create an app.js file that will contain the camera and QR detection logic:

const video = document.getElementById("video");
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

const startBtn = document.getElementById("startBtn");
const stopBtn = document.getElementById("stopBtn");
const restartBtn = document.getElementById("restartBtn");
const fileInput = document.getElementById("fileInput");
const resultEl = document.getElementById("result");

let stream = null;
let scanning = false;
let rafId = null;

async function startCamera() {
  try {
    stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } });
    video.srcObject = stream;
    scanning = true;
    resultEl.textContent = "Scanning...";
    startBtn.disabled = true;
    stopBtn.disabled = false;
    restartBtn.disabled = true;
    scanLoop();
  } catch (err) {
    alert("Error accessing camera: " + err.message);
  }
}

function stopCamera() {
  scanning = false;
  if (rafId) {
    cancelAnimationFrame(rafId);
    rafId = null;
  }
  if (stream) {
    video.srcObject = null;
    stream.getTracks().forEach(track => track.stop());
    stream = null;
  }
  video.pause();
  startBtn.disabled = false;
  stopBtn.disabled = true;
  restartBtn.disabled = false;
}

function restartCamera() {
  stopCamera();
  startCamera();
}

function scanLoop() {
  if (!scanning) return;

  if (video.readyState === video.HAVE_ENOUGH_DATA) {
    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;
    ctx.drawImage(video, 0, 0, canvas.width, canvas.height);

    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    const code = jsQR(imageData.data, imageData.width, imageData.height);

    if (code) {
      resultEl.textContent = "QR Code: " + code.data;
      resultEl.classList.add("flash");

      stopCamera(); // auto stop after detection

      setTimeout(() => resultEl.classList.remove("flash"), 1000);
      return;
    }
  }
  rafId = requestAnimationFrame(scanLoop);
}

function scanImageFile(file) {
  const img = new Image();
  img.onload = () => {
    canvas.width = img.width;
    canvas.height = img.height;
    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    const code = jsQR(imageData.data, imageData.width, imageData.height);

    if (code) {
      resultEl.textContent = "QR Code (from file): " + code.data;
      resultEl.classList.add("flash");
      setTimeout(() => resultEl.classList.remove("flash"), 1000);
    } else {
      resultEl.textContent = "No QR code found in image.";
    }
  };
  img.src = URL.createObjectURL(file);
}

startBtn.addEventListener("click", startCamera);
stopBtn.addEventListener("click", stopCamera);
restartBtn.addEventListener("click", restartCamera);
fileInput.addEventListener("change", e => {
  if (e.target.files.length > 0) {
    scanImageFile(e.target.files[0]);
  }
});

Let’s take a closer look at the code:

  • stream stores the camera stream.
  • scanning tracks if scanning is active.
  • rafId allows us to stop the loop when needed.
  • { facingMode: "environment" } prefers the rear camera on phones.
  • The loop using requestAnimationFrame captures each video frame, passes it to jsQR, and checks for a QR code. If it detects one, it displays the result and automatically stops the camera.
  • The event listeners hook up the UI buttons and file input to the respective functions.

Now run your app and test its scanning functionalities.

Scanning a QR code with the jsQR library

Approach B: Using the qr-scanner library

The qr-scanner library comes with overlays, worker offloading, flash support, and easy image scanning integration – which means less manual coding.

We’ll create our QR code scanner app in two steps:

  1. Defining the HTML structure and implementing the JavaScript scanner logic
  2. Adding basic CSS styling

Let’s get started!

Step 1: Define the HTML structure and implement the JavaScript scanner logic

In your project folder, create your index.html file. This time, we’ll use it to implement the JavaScript logic as well:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>qr-scanner Demo</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>
  <main class="wrap">
    <h2>qr-scanner Demo</h2>

    <div class="controls">
      <button id="startBtn">Start camera</button>
      <button id="switchBtn" disabled>Switch camera</button>
      <button id="flashBtn" disabled>Toggle flash</button>
      <button id="stopBtn" disabled>Stop</button>
      <label class="file">
        <input id="fileInput" type="file" accept="image/*" />
        <span>Scan from image…</span>
      </label>
    </div>

    <div class="stage">
      <video id="video" playsinline muted></video>
      <!-- qr-scanner can render its own overlays if enabled -->
      <div id="overlayContainer"></div>
    </div>

    <p class="result"><strong>Result:</strong> <span id="result">—</span></p>
  </main>

  <script type="module">
    import QrScanner from 'https://unpkg.com/qr-scanner@1.4.2/qr-scanner.min.js';

    const video = document.getElementById('video');
    const overlayContainer = document.getElementById('overlayContainer');
    const resultEl = document.getElementById('result');

    const startBtn = document.getElementById('startBtn');
    const switchBtn = document.getElementById('switchBtn');
    const flashBtn = document.getElementById('flashBtn');
    const stopBtn = document.getElementById('stopBtn');
    const fileInput = document.getElementById('fileInput');

    let facingMode = 'environment'; // 'user'
    let qrScanner = null;

    function setButtons(state) {
      const running = state === 'running';
      startBtn.disabled = running;
      switchBtn.disabled = !running;
      stopBtn.disabled = !running;
      // flashBtn gets enabled dynamically after start() if supported
    }

    function makeScanner() {
      if (qrScanner) {
        qrScanner.stop();
      }
      qrScanner = new QrScanner(
        video,
        (result) => {
          // If options are provided, result is an object with { data, cornerPoints }
          resultEl.textContent = typeof result === 'string' ? result : result.data;
        },
        {
          preferredCamera: facingMode,           // 'environment' or 'user'
          highlightScanRegion: true,             // draw the scan region
          highlightCodeOutline: true,            // draw outline on detected code
          returnDetailedScanResult: true,        // ensure { data, cornerPoints }
        }
      );
    }

    async function start() {
      try {
        if (!qrScanner) makeScanner();
        await qrScanner.start(); // prompts for permission if needed

        // Enable/disable flash button depending on support
        const hasFlash = await qrScanner.hasFlash();
        flashBtn.disabled = !hasFlash;

        setButtons('running');
      } catch (e) {
        console.error(e);
        alert('Could not start the camera. Ensure https:// or http://localhost and grant camera access.');
      }
    }

    async function stop() {
      if (qrScanner) await qrScanner.stop();
      setButtons('idle');
      flashBtn.disabled = true;
    }

    async function switchCamera() {
      facingMode = (facingMode === 'environment') ? 'user' : 'environment';
      makeScanner();
      await start();
    }

    async function toggleFlash() {
      try {
        const on = await qrScanner.isFlashOn();
        await qrScanner.turnFlash(!on);
      } catch {
        // Ignore if not supported
      }
    }

    // Scan a still image
    fileInput.addEventListener('change', async (e) => {
      const file = e.target.files?.[0];
      if (!file) return;
      try {
        const res = await QrScanner.scanImage(file, { returnDetailedScanResult: true });
        resultEl.textContent = res.data;
      } catch {
        resultEl.textContent = 'No QR found in image';
      }
    });

    // Wire up UI
    startBtn.addEventListener('click', start);
    stopBtn.addEventListener('click', stop);
    switchBtn.addEventListener('click', switchCamera);
    flashBtn.addEventListener('click', toggleFlash);

    // Initialize UI
    setButtons('idle');
  </script>
</body>
</html>

This code does the following:

  • It imports the ES module build from a CDN. According to the library’s README, this module auto-loads its web worker via dynamic import (no manual worker path needed) and supports HTTPS-only camera scanning.
  • new QrScanner(video, onDecode, options) constructs the scanner. The key options used are:
  • preferredCamera: 'environment' | 'user' to choose the rear/front camera.
  • highlightScanRegion + highlightCodeOutline to add nice overlays.
  • returnDetailedScanResult: true to ensure the callback gets { data, cornerPoints }.
  • qrScanner.start() / stop() controls the camera.
  • qrScanner.hasFlash(), turnFlash(true/false), and isFlashOn() allow toggling the flash on supported devices.
  • Switching cameras is implemented by recreating the scanner with the opposite preferredCamera and starting it again.
  • QrScanner.scanImage(file) decodes still images without the camera.

Step 2: Add basic CSS styling

To make our app look nice, let’s also create a style.css with the following contents:

:root { --w: min(92vw, 720px); }

body {
  font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
  margin: 0;
  padding: 2rem;
  background: #fafafa;
  color: #222;
}

.wrap {
  margin: 0 auto;
  width: var(--w);
}

h2 {
  margin-bottom: 1rem;
  font-size: 1.5rem;
  font-weight: 600;
}

.controls {
  display: flex;
  gap: .5rem;
  flex-wrap: wrap;
  margin-bottom: .75rem;
}

button, .file {
  padding: .5rem .75rem;
  border: none;
  border-radius: 6px;
  background: #0057e7;
  color: white;
  cursor: pointer;
  font-size: .9rem;
  transition: background .2s;
}

button:disabled {
  background: #ccc;
  cursor: not-allowed;
}

button:hover:enabled,
.file:hover {
  background: #003f9e;
}

.file {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
}

.file input {
  display: none;
}

.stage {
  position: relative;
  width: 100%;
  aspect-ratio: 3 / 4;
  background: #111;
  border-radius: 12px;
  overflow: hidden;
}

video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

#overlayContainer {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.result {
  margin-top: .75rem;
  font-size: 1.05rem;
  padding: .5rem;
  background: #f0f0f0;
  border-radius: 6px;
}

This achieves the following:

  • .file aligns label text and icon side by side.
  • input[type=file] is hidden (only the styled label shows).
  • aspect-ratio: 3/4 keeps it proportional on all screens.
  • overflow: hidden clips the video/overlay to the rounded shape.
  • pointer-events: none means that clicks go through to the video area.

Now run your app and scan a QR code.

Scanning a QR code with the qr-scanner library

Conclusion

This concludes our tutorial on how to set up a simple QR code scanning web app using jsQR and qr-scanner.

Using open-source libraries like these can be great for prototyping and personal projects. However, this approach is rarely viable for developing business solutions, since the work involved in maintaining an app without dedicated support is unpredictable.

We developed the Scanbot Web Barcode Scanner SDK to help enterprises overcome the hurdles presented by community-driven scanning software. Our goal was to provide a developer-friendly solution for a wide range of platforms that consistently delivers high-quality results – even in challenging circumstances.

In the following tutorial, we’ll show you how to set up a fully functional QR code scanning web app in a single HTML file using the Scanbot SDK.

Building a JavaScript QR code scanner with the Scanbot Web Barcode Scanner SDK

We’ll create our QR code scanner app in two steps:

  1. Setting up the barcode scanner
  2. Optimizing the scanner for QR codes

There’s no need to manually code any UI elements, since the SDK comes with Ready-to-Use UI Components.

Scanning a QR code with the default configuration
Scanning a QR code with the default configuration
Scanning a QR code with the AR overlay and confirmation dialog enabled
Scanning a QR code with the AR overlay and confirmation dialog enabled

Step 1: Set up the barcode scanner

In your project folder, create an index.html with some boilerplate code.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta
            name="viewport"
            content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
        />
        <title>QR Code Scanner</title>
    </head>
    <body>    
    </body>
</html>

Now we’ll need to do the following:

  1. Create a button that calls up the scanning interface when clicked.
  2. Include a <p> element on the page for displaying the scanning result.
  3. Import the Scanbot Web SDK using a CDN.
  4. Process the scan result before displaying it on the page.

⚠️ In this tutorial, we’re importing the SDK via jsDelivr. However, you should only do this for quick prototyping. In your production environment, please download the Web SDK directly (or install it via npm) and include its files in your project.

Your index.html will look something like this:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta
            name="viewport"
            content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
        />
        <title>QR Code Scanner</title>
    </head>

    <body style="margin: 0">
        <button id="start-scanning">Start scanning</button>
        <p id="result"></p>
        <script type="module">
            import "https://cdn.jsdelivr.net/npm/scanbot-web-sdk@7.0.0/bundle/ScanbotSDK.ui2.min.js";
            const sdk = await ScanbotSDK.initialize({
                enginePath:
                    "https://cdn.jsdelivr.net/npm/scanbot-web-sdk@7.0.0/bundle/bin/barcode-scanner/",
            });
            document
                .getElementById("start-scanning")
                .addEventListener("click", async () => {

                    const config =
                        new ScanbotSDK.UI.Config.BarcodeScannerScreenConfiguration();

                    const scanResult = await ScanbotSDK.UI.createBarcodeScanner(config);
                    if (scanResult?.items?.length > 0) {
                        document.getElementById("result").innerText =
                            `Barcode type: ${scanResult.items[0].barcode.format} \n` +
                            `Barcode content: "${scanResult.items[0].barcode.text}" \n`;
                    } else {
                        document.getElementById("result").innerText = "Scanning aborted by the user";
                    }
                });
        </script>
    </body>
</html>

This already provides us with a fully functional barcode scanner – but we can slightly adjust its configuration to make it even better suited for QR codes. Feel free to run the app and test its functionalities.

Scanning a QR code with the default configuration

Step 2: Optimize the scanner for QR codes

As it is now, our scanner will read any barcode type. Restricting it to scanning only QR codes prevents unintended scans and improves the scanner’s performance, as it doesn’t need to check each supported symbology.

So let’s implement this by modifying the BarcodeScannerScreenConfiguration we assigned to the config constant in the previous step.

config.scannerConfiguration.barcodeFormats = ["QR_CODE"];

We can also add additional quality-of-life features. In this example, we’ll replace the viewfinder with an AR overlay that users can tap on to select which code they would like to scan. The value is then presented in a confirmation dialog.

// Disable the viewfinder
config.viewFinder.visible = false;

// Enable the AR overlay
config.useCase.arOverlay.visible = true;

// Let users tap on the QR code they would like to scan
config.useCase.arOverlay.automaticSelectionEnabled = false;

// Show the scanned code's value in a confirmation dialog
config.useCase.confirmationSheetEnabled = true;

Your final index.html will look like this:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta
            name="viewport"
            content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
        />
        <title>QR Code Scanner</title>
    </head>

    <body style="margin: 0">
        <button id="start-scanning">Start scanning</button>
        <p id="result"></p>
        <script type="module">
            import "https://cdn.jsdelivr.net/npm/scanbot-web-sdk@7.0.0/bundle/ScanbotSDK.ui2.min.js";
            const sdk = await ScanbotSDK.initialize({
                enginePath:
                    "https://cdn.jsdelivr.net/npm/scanbot-web-sdk@7.0.0/bundle/bin/barcode-scanner/",
            });
            document
                .getElementById("start-scanning")
                .addEventListener("click", async () => {

                    const config =
                        new ScanbotSDK.UI.Config.BarcodeScannerScreenConfiguration();
                        config.scannerConfiguration.barcodeFormats = ["QR_CODE"];
                        config.viewFinder.visible = false;
                        config.useCase.arOverlay.visible = true;
                        config.useCase.arOverlay.automaticSelectionEnabled = false;
                        config.useCase.confirmationSheetEnabled = true;

                    const scanResult = await ScanbotSDK.UI.createBarcodeScanner(config);
                    if (scanResult?.items?.length > 0) {
                        document.getElementById("result").innerText =
                            `Barcode type: ${scanResult.items[0].barcode.format} \n` +
                            `Barcode content: "${scanResult.items[0].barcode.text}" \n`;
                    } else {
                        document.getElementById("result").innerText = "Scanning aborted by the user";
                    }
                });
        </script>
    </body>
</html>

Now run the app again and scan a QR code …

QR Code

… to test your scanner!

Scanning a QR code with the AR overlay and confirmation dialog enabled

💡 To test your web app on your phone, you have a few options. One option is to use a service like ngrok, which creates a tunnel to one of their SSL-certified domains. Their Quick Start guide will help you get up and running quickly.

Conclusion

This is just one of the many scanner configurations the Scanbot SDK has to offer – take a look at the RTU UI documentation and API reference to learn more.

Integration guides are also available for the following Web frameworks:

Should you have questions about this tutorial or run into any issues, we’re happy to help! Just shoot us an email via tutorial-support@scanbot.io.

Happy scanning! 🤳

Related blog posts

Experience our demo apps

Barcode Icon Art

Barcode Scanner SDK

Scan 1D and 2D barcodes reliably in under 0.04s. Try features like Batch Scanning, Scan & Count, and our AR Overlays.

Launch Web Demo

Scan the code to launch the web demo on your phone.

Web QR Code

Also available to download from:

Document Icon Art

Document Scanner SDK

Scan documents quickly and accurately with our free demo app. Create crisp digital scans in seconds.

Launch Web Demo

Scan the code to launch the web demo on your phone.

Black and white QR code. Scan this code for quick access to information.

Also available to download from:

Data_capture Icon Art

Data Capture Modules

Try fast, accurate data capture with our demo app. Extract data from any document instantly – 100% secure.

Launch Web Demo

Scan the code to launch the web demo on your phone.

Black and white QR code. Scan this quick response code with your smartphone.

Also available to download from: