Scanbot SDK has been acquired by Apryse! Learn more

Learn more
Skip to content

How to use instascan.js to build a WebRTC-based QR code scanner with HTML5

Kevin November 7, 2025 13 mins read
Instascan WebRTC QR code scanner HTML5 tutorial

In this tutorial, we’ll build a web app optimized for mobile devices that recognizes and reads QR codes and displays their values to the user.

To implement the scanning functionalities, we’ll use Instascan, an HTML5-based JavaScript library that uses WebRTC technology for camera access and provides an API for starting the camera, selecting from available devices, and listening for scanned QR code events.

We’ll follow these steps:

  1. Creating the basic HTML structure
  2. Adding the Instascan library
  3. Adding the video element
  4. Adding a display area for scanned QR codes
  5. Initializing the scanner with JavaScript
  6. Handling scan events
  7. Accessing the camera
  8. Adding styling
  9. Running the app
Scanning QR codes with our WebRTC-based instascan web app

Step 1: Create the basic HTML structure

Start with a standard HTML5 document and add a camera selector element in the <body>. This gives users the option to use their mobile device’s back- or front-facing camera.

<!DOCTYPE html>
<html>
  <head>
    <title>Instascan QR Code Scanner</title>
  </head>
  <body>
    <h1>Instascan QR Code Scanner</h1>
    <select id="cameraSelector" style="display:none;">
      <option value="">Select Camera...</option>
    </select>
  </body>
</html>

💡 In this example, we’re going to include the JavaScript and CSS in the HTML file. However, feel free to separate them into external files.

Step 2: Add the Instascan library

To use instascan.js in your web app, you can add it to the <head> using a CDN.

<script
  type="text/javascript"
  src="https://rawcdn.githack.com/schmich/instascan-builds/master/instascan.min.js"
></script>

If you’d rather not use a CDN, you can also download the instascan.min.js file from the repository’s releases page, put it in your project directory, and change the script source to src="instascan.min.js".

Step 3: Add the video element

Next, create a <video> element to show the camera feed. Add autoplay and playsinline attributes so it starts automatically and works on mobile:

<video id="preview" autoplay playsinline></video>

Step 4: Add a display area for scanned QR codes

Below the video, add a heading and a container <div> to hold the list of scanned codes.

<h2>Scanned QR Codes:</h2>
<div id="scannedList">
  <p style="color: #666;">No codes scanned yet...</p>
</div>

Step 5: Initialize the scanner with JavaScript

Start another <script> tag and get references to the preview, scannedList and cameraSelector elements. Also create a new scanner instance, passing in the video element.

  const videoElement = document.getElementById("preview");
  const scannedList = document.getElementById("scannedList");
  const cameraSelector = document.getElementById("cameraSelector");
  const scanner = new Instascan.Scanner({ 
    video: videoElement,
    mirror: false
  });

  let cameras = [];

Step 6: Handle scan events

Add a listener that fires whenever a QR code is detected. In this example, it will do the following for each scan:

  • Get the current time
  • Create a new <div> element styled as a list item
  • Add the scanned content and timestamp
  • Remove the placeholder text on first scan
  • Insert the new item at the top of the list
scanner.addListener("scan", (content) => {
  const timestamp = new Date().toLocaleTimeString();
  
  const item = document.createElement("div");
  item.className = "scanned-item";
  item.innerHTML = `<strong>${content}</strong><span class="timestamp">${timestamp}</span>`;
  
  if (scannedList.querySelector("p")) {
    scannedList.innerHTML = "";
  }
  
  scannedList.insertBefore(item, scannedList.firstChild);
});

Step 7: Access the camera

Next, add a listener to handle camera switching.

cameraSelector.addEventListener("change", (e) => {
  const cameraId = e.target.value;
  const selectedCamera = cameras.find(c => c.id === cameraId);
  if (selectedCamera) {
    scanner.start(selectedCamera);
  }
});

Now you can use the Instascan API to get the available cameras, then start scanning:

Instascan.Camera.getCameras()
  .then((availableCameras) => {
    cameras = availableCameras;
    
    if (cameras.length === 0) {
      alert("No cameras found.");
      return;
    }
    
    // Populate dropdown
    cameras.forEach((camera, index) => {
      const option = document.createElement("option");
      option.value = camera.id;
      option.text = camera.name || `Camera ${index + 1}`;
      cameraSelector.appendChild(option);
    });
    
    // Show selector if multiple cameras
    if (cameras.length > 1) {
      cameraSelector.style.display = "block";
    }
    
    // Try to find and start back camera
    let backCamera = null;
    
    // Try multiple methods to find back camera
    for (let camera of cameras) {
      const name = camera.name.toLowerCase();
      // Check for common back camera indicators
      if (name.includes("back") || 
          name.includes("rear") || 
          name.includes("environment") ||
          name.includes("后") ||  // Chinese
          name.includes("trasera")) {  // Spanish
        backCamera = camera;
        break;
      }
    }
    
    // If no back camera found, try the last camera (often the back one on mobile)
    if (!backCamera && cameras.length > 1) {
      backCamera = cameras[cameras.length - 1];
    }
    
    // Fallback to first camera
    const cameraToUse = backCamera || cameras[0];
    
    // Set dropdown to selected camera
    cameraSelector.value = cameraToUse.id;
    
    // Start scanning
    scanner.start(cameraToUse);
    
    console.log("Available cameras:", cameras.map(c => c.name));
    console.log("Using camera:", cameraToUse.name);
  })
  .catch((err) => {
    console.error("Camera error:", err);
    alert("Could not access camera. Please check permissions.");
  });

💡 The Instascan library uses WebRTC under the hood, even though you don’t see explicit API calls in this code.

Instascan internally uses the WebRTC API navigator.mediaDevices.getUserMedia() to request camera access and the MediaStream API to handle the video stream from the camera.

When you call Instascan.Camera.getCameras() and scanner.start(), Instascan is internally doing something like this:

navigator.mediaDevices.getUserMedia({ 
  video: { facingMode: "environment" }
})
.then(stream => {
  videoElement.srcObject = stream;
})

Why this matters:

  • HTTPS requirement: WebRTC camera access only works on:
    • https:// websites
    • localhost (for testing)
    • But not on http:// (except localhost)
  • Browser permissions: Users must grant camera permission
  • Browser support: Needs a WebRTC-compatible browser (all modern browsers, but not IE)
  • Security context: Some features may not work in iframes or certain contexts

If you wanted direct WebRTC control, you could replace Instascan with manual camera access:

navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } })
  .then(stream => {
    videoElement.srcObject = stream;
    // Then use a different QR library to scan the video frames
  });

Step 8 (optional): Add styling

To improve the app’s looks, you can add CSS in the <head>:

<style>
  body {
    font-family: Arial, sans-serif;
    margin: 20px;
  }
  
  #preview {
    width: 100%;
    max-width: 400px;
    height: auto;
    border: 2px solid #ccc;
    border-radius: 5px;
  }
  
  #cameraSelector {
    margin: 10px 0;
    padding: 8px;
    font-size: 16px;
  }
  
  #scannedList {
    margin-top: 20px;
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
    background-color: #f9f9f9;
    max-width: 400px;
  }
  .scanned-item {
    padding: 8px;
    margin: 5px 0;
    background-color: white;
    border-left: 3px solid #4CAF50;
    word-break: break-all;
  }
  .timestamp {
    font-size: 0.8em;
    color: #666;
    margin-left: 5px;
  }
</style>

Your final index.html will look like this:

<!DOCTYPE html>
<html>
  <head>
    <title>Instascan QR Code Scanner</title>
    <script
      type="text/javascript"
      src="https://rawcdn.githack.com/schmich/instascan-builds/master/instascan.min.js"
    ></script>
    <style>
      body {
        font-family: Arial, sans-serif;
        margin: 20px;
      }
      
      #preview {
        width: 100%;
        max-width: 400px;
        height: auto;
        border: 2px solid #ccc;
        border-radius: 5px;
      }
      
      #cameraSelector {
        margin: 10px 0;
        padding: 8px;
        font-size: 16px;
      }
      
      #scannedList {
        margin-top: 20px;
        padding: 10px;
        border: 1px solid #ccc;
        border-radius: 5px;
        background-color: #f9f9f9;
        max-width: 400px;
      }
      .scanned-item {
        padding: 8px;
        margin: 5px 0;
        background-color: white;
        border-left: 3px solid #4CAF50;
        word-break: break-all;
      }
      .timestamp {
        font-size: 0.8em;
        color: #666;
        margin-left: 5px;
      }
    </style>
  </head>
  <body>
    <h1>Instascan QR Code Scanner</h1>
    <select id="cameraSelector" style="display:none;">
      <option value="">Select Camera...</option>
    </select>
<video id="preview" autoplay playsinline></video>

    <h2>Scanned QR Codes:</h2>
    <div id="scannedList">
      <p style="color: #666;">No codes scanned yet...</p>
    </div>

    <script>
      const videoElement = document.getElementById("preview");
      const scannedList = document.getElementById("scannedList");
      const cameraSelector = document.getElementById("cameraSelector");
      const scanner = new Instascan.Scanner({ 
        video: videoElement,
        mirror: false
      });

      let cameras = [];

      scanner.addListener("scan", (content) => {
        const timestamp = new Date().toLocaleTimeString();
        
        const item = document.createElement("div");
        item.className = "scanned-item";
        item.innerHTML = `<strong>${content}</strong><span class="timestamp">${timestamp}</span>`;
        
        if (scannedList.querySelector("p")) {
          scannedList.innerHTML = "";
        }
        
        scannedList.insertBefore(item, scannedList.firstChild);
      });

      cameraSelector.addEventListener("change", (e) => {
        const cameraId = e.target.value;
        const selectedCamera = cameras.find(c => c.id === cameraId);
        if (selectedCamera) {
          scanner.start(selectedCamera);
        }
      });

      Instascan.Camera.getCameras()
        .then((availableCameras) => {
          cameras = availableCameras;
          
          if (cameras.length === 0) {
            alert("No cameras found.");
            return;
          }
          
          cameras.forEach((camera, index) => {
            const option = document.createElement("option");
            option.value = camera.id;
            option.text = camera.name || `Camera ${index + 1}`;
            cameraSelector.appendChild(option);
          });
          
          if (cameras.length > 1) {
            cameraSelector.style.display = "block";
          }
          
          let backCamera = null;
          
          for (let camera of cameras) {
            const name = camera.name.toLowerCase();
            if (name.includes("back") || 
                name.includes("rear") || 
                name.includes("environment") ||
                name.includes("后") ||
                name.includes("trasera")) {
              backCamera = camera;
              break;
            }
          }
          
          if (!backCamera && cameras.length > 1) {
            backCamera = cameras[cameras.length - 1];
          }
          
          const cameraToUse = backCamera || cameras[0];
          
          cameraSelector.value = cameraToUse.id;
          
          scanner.start(cameraToUse);
          
          console.log("Available cameras:", cameras.map(c => c.name));
          console.log("Using camera:", cameraToUse.name);
        })
        .catch((err) => {
          console.error("Camera error:", err);
          alert("Could not access camera. Please check permissions.");
        });
    </script>
  </body>
</html>

Step 9: Run the app

Your WebRTC-based QR code scanner is ready! Run it and scan some QR codes.

Scanning QR codes with our WebRTC-based, instascan-powered web app

💡 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 concludes our tutorial on how to set up a simple QR code scanning web app in HTML5 with Instascan.

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 an HTML5-based QR code scanner app 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

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 references 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: