← Back to DevBytes

How to Build a QR Code Generator with JavaScript

What is a QR Code Generator?

A QR code (Quick Response code) is a two-dimensional barcode that stores data in a matrix of black and white squares. A QR code generator is a tool that converts text, URLs, contact information, or any string data into a scannable QR code image. When you scan it with a smartphone camera, the encoded data is instantly decoded and made available — typically opening a website, displaying text, or adding a contact.

With JavaScript running directly in the browser, you can build a fully functional QR code generator that works entirely on the client side — no server required. Users type in a URL or text, click a button, and a QR code appears instantly on screen, ready to download or share.

Why Build a QR Code Generator with JavaScript?

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Building your own QR code generator gives you complete control over the user experience. Here's why it matters:

Core Approaches to QR Code Generation

There are two practical ways to generate QR codes in JavaScript. Let's explore both so you can choose the one that fits your project.

Option 1: Using the qrcode.js Library (CDN)

The most straightforward approach is to use a well-tested open-source library. The qrcode.js library by David Shim handles all the complex QR encoding mathematics — Reed-Solomon error correction, mask pattern selection, and module placement — behind a simple API. Include it via CDN and you can generate QR codes with just a few lines of code.

Include the library in your HTML with a script tag:

<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>

Then create a QR code by instantiating the QRCode class:

// Select the container element
const container = document.getElementById('qrcode-container');

// Generate QR code
const qrcode = new QRCode(container, {
  text: 'https://example.com',
  width: 256,
  height: 256,
  colorDark: '#000000',
  colorLight: '#ffffff',
  correctLevel: QRCode.CorrectLevel.H
});

The library renders the QR code directly into the container as an HTML image element wrapped in a canvas. The correctLevel option sets error correction — L (7%), M (15%), Q (25%), or H (30%). Higher levels make the code more resistant to damage but increase its density.

Option 2: Using the qrcode npm Package with a Bundler

If you're working with a modern build system like Webpack, Vite, or Parcel, the qrcode npm package by Soldair offers a more modular API with Promise-based output and support for multiple output formats — canvas, SVG, PNG data URL, and even terminal-friendly ASCII output.

Install it first:

npm install qrcode

Then import and use it in your JavaScript module:

import QRCode from 'qrcode';

// Generate QR code as a data URL
QRCode.toDataURL('https://example.com', {
  width: 300,
  margin: 2,
  color: {
    dark: '#000000',
    light: '#ffffff'
  }
}).then(url => {
  document.getElementById('qrcode-img').src = url;
}).catch(err => {
  console.error('QR generation failed:', err);
});

This package also supports SVG output, which scales perfectly at any resolution — ideal for print-ready QR codes:

QRCode.toString('Hello World', { type: 'svg' }).then(svgString => {
  document.getElementById('qrcode-svg').innerHTML = svgString;
});

Building a Complete QR Code Generator App

Let's build a fully functional QR code generator from scratch. We'll use the CDN-based qrcode.js library for simplicity. The app will include an input field, real-time generation, a download button, and customization controls for size and colors.

HTML Structure

Start with a clean HTML skeleton that includes all the UI elements we need — an input area, configuration controls, a display canvas, and action buttons.

<div class="qr-generator">
  <h2>QR Code Generator</h2>
  
  <div class="input-section">
    <label for="qr-input">Enter text or URL</label>
    <input 
      type="text" 
      id="qr-input" 
      placeholder="https://example.com" 
      autocomplete="off"
    >
    <p class="char-count"><span id="count">0</span> characters</p>
  </div>
  
  <div class="controls">
    <div class="control-group">
      <label for="qr-size">Size (px)</label>
      <input type="range" id="qr-size" min="128" max="512" value="256" step="8">
      <span id="size-value">256</span>
    </div>
    
    <div class="control-group">
      <label for="qr-dark">Foreground Color</label>
      <input type="color" id="qr-dark" value="#000000">
    </div>
    
    <div class="control-group">
      <label for="qr-light">Background Color</label>
      <input type="color" id="qr-light" value="#ffffff">
    </div>
    
    <div class="control-group">
      <label for="qr-correction">Error Correction</label>
      <select id="qr-correction">
        <option value="L">Low (7%)</option>
        <option value="M" selected>Medium (15%)</option>
        <option value="Q">Quartile (25%)</option>
        <option value="H">High (30%)</option>
      </select>
    </div>
  </div>
  
  <div class="output-section">
    <div id="qrcode-container" class="qrcode-display"></div>
    <div id="placeholder-message" class="placeholder">
      Enter text above to generate a QR code
    </div>
  </div>
  
  <div class="actions">
    <button id="download-btn" class="btn-primary" disabled>
      Download PNG
    </button>
    <button id="clear-btn" class="btn-secondary">
      Clear
    </button>
  </div>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>

CSS Styling

Add styles that make the generator clean, responsive, and pleasant to use. The CSS below uses a modern card-based layout with subtle shadows and smooth transitions.

<style>
  * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }
  
  .qr-generator {
    max-width: 540px;
    margin: 40px auto;
    padding: 32px;
    background: #ffffff;
    border-radius: 16px;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  }
  
  .qr-generator h2 {
    font-size: 24px;
    font-weight: 700;
    color: #1a1a2e;
    margin-bottom: 24px;
    text-align: center;
  }
  
  .input-section {
    margin-bottom: 20px;
  }
  
  .input-section label {
    display: block;
    font-size: 14px;
    font-weight: 600;
    color: #4a4a6a;
    margin-bottom: 6px;
  }
  
  .input-section input[type="text"] {
    width: 100%;
    padding: 14px 16px;
    font-size: 16px;
    border: 2px solid #e2e2e8;
    border-radius: 8px;
    outline: none;
    transition: border-color 0.2s ease;
  }
  
  .input-section input[type="text"]:focus {
    border-color: #4f46e5;
    box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
  }
  
  .char-count {
    font-size: 12px;
    color: #888;
    margin-top: 4px;
    text-align: right;
  }
  
  .controls {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px;
    margin-bottom: 24px;
  }
  
  .control-group {
    display: flex;
    flex-direction: column;
    gap: 4px;
  }
  
  .control-group label {
    font-size: 13px;
    font-weight: 600;
    color: #4a4a6a;
  }
  
  .control-group input[type="range"] {
    width: 100%;
    cursor: pointer;
  }
  
  .control-group input[type="color"] {
    width: 48px;
    height: 32px;
    border: 1px solid #d4d4dc;
    border-radius: 4px;
    cursor: pointer;
    padding: 2px;
  }
  
  .control-group select {
    padding: 8px 10px;
    font-size: 14px;
    border: 2px solid #e2e2e8;
    border-radius: 6px;
    background: #fff;
    cursor: pointer;
    outline: none;
  }
  
  .control-group select:focus {
    border-color: #4f46e5;
  }
  
  #size-value {
    font-size: 13px;
    color: #666;
    margin-left: 4px;
  }
  
  .output-section {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 280px;
    background: #fafafc;
    border: 2px dashed #e2e2e8;
    border-radius: 12px;
    margin-bottom: 20px;
    position: relative;
    overflow: hidden;
  }
  
  .qrcode-display {
    display: flex;
    justify-content: center;
    align-items: center;
  }
  
  .qrcode-display img {
    border-radius: 8px;
  }
  
  .placeholder {
    color: #aaa;
    font-size: 15px;
    text-align: center;
    padding: 20px;
  }
  
  .actions {
    display: flex;
    gap: 12px;
    justify-content: center;
  }
  
  .btn-primary, .btn-secondary {
    padding: 12px 24px;
    font-size: 15px;
    font-weight: 600;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    transition: all 0.2s ease;
  }
  
  .btn-primary {
    background: #4f46e5;
    color: #ffffff;
  }
  
  .btn-primary:hover:not(:disabled) {
    background: #4338ca;
    transform: translateY(-1px);
  }
  
  .btn-primary:disabled {
    background: #c4c4d0;
    cursor: not-allowed;
  }
  
  .btn-secondary {
    background: #f4f4f8;
    color: #4a4a6a;
  }
  
  .btn-secondary:hover {
    background: #e8e8f0;
  }
  
  @media (max-width: 600px) {
    .qr-generator {
      margin: 16px;
      padding: 20px;
    }
    .controls {
      grid-template-columns: 1fr;
    }
  }
</style>

JavaScript Logic — Full Implementation

Now for the core JavaScript. This code handles real-time QR generation, updates all configuration options reactively, manages the download functionality, and gracefully handles empty input states. Every function is commented for clarity.

<script>
  // DOM element references
  const inputField = document.getElementById('qr-input');
  const container = document.getElementById('qrcode-container');
  const placeholderMsg = document.getElementById('placeholder-message');
  const downloadBtn = document.getElementById('download-btn');
  const clearBtn = document.getElementById('clear-btn');
  const sizeSlider = document.getElementById('qr-size');
  const sizeValue = document.getElementById('size-value');
  const darkColorInput = document.getElementById('qr-dark');
  const lightColorInput = document.getElementById('qr-light');
  const correctionSelect = document.getElementById('qr-correction');
  const charCountSpan = document.getElementById('count');
  
  // State variables
  let currentQRCode = null;
  let currentQRDataURL = null;
  let debounceTimer = null;
  
  // --- Helper: Update character count ---
  function updateCharCount() {
    const length = inputField.value.length;
    charCountSpan.textContent = length;
    if (length > 1000) {
      charCountSpan.style.color = '#e74c3c';
    } else {
      charCountSpan.style.color = '#888';
    }
  }
  
  // --- Helper: Get current configuration ---
  function getConfig() {
    return {
      text: inputField.value.trim(),
      width: parseInt(sizeSlider.value),
      height: parseInt(sizeSlider.value),
      colorDark: darkColorInput.value,
      colorLight: lightColorInput.value,
      correctLevel: QRCode.CorrectLevel[correctionSelect.value]
    };
  }
  
  // --- Core: Generate QR code ---
  function generateQRCode() {
    const config = getConfig();
    
    // Clear previous QR code
    container.innerHTML = '';
    currentQRCode = null;
    currentQRDataURL = null;
    
    if (!config.text) {
      // No input — show placeholder
      placeholderMsg.style.display = 'block';
      downloadBtn.disabled = true;
      return;
    }
    
    placeholderMsg.style.display = 'none';
    
    try {
      // Create QR code instance
      currentQRCode = new QRCode(container, {
        text: config.text,
        width: config.width,
        height: config.height,
        colorDark: config.colorDark,
        colorLight: config.colorLight,
        correctLevel: config.correctLevel
      });
      
      downloadBtn.disabled = false;
      
      // Extract data URL from the generated canvas for download
      setTimeout(() => {
        const canvas = container.querySelector('canvas');
        if (canvas) {
          currentQRDataURL = canvas.toDataURL('image/png');
        }
        // Also check for img element (some versions render as img)
        const img = container.querySelector('img');
        if (img && !currentQRDataURL) {
          currentQRDataURL = img.src;
        }
      }, 50);
      
    } catch (error) {
      console.error('QR generation error:', error);
      container.innerHTML = '<p style="color:red;">Failed to generate QR code.</p>';
      downloadBtn.disabled = true;
    }
  }
  
  // --- Debounced input handler ---
  function onInputChange() {
    updateCharCount();
    
    // Clear any pending debounce timer
    if (debounceTimer) {
      clearTimeout(debounceTimer);
    }
    
    // Debounce QR generation by 300ms for smooth typing
    debounceTimer = setTimeout(() => {
      generateQRCode();
    }, 300);
  }
  
  // --- Immediate regeneration (for color/size changes) ---
  function onConfigChange() {
    if (debounceTimer) {
      clearTimeout(debounceTimer);
    }
    generateQRCode();
  }
  
  // --- Download handler ---
  function downloadQRCode() {
    if (!currentQRDataURL) {
      // Try to extract again
      const canvas = container.querySelector('canvas');
      if (canvas) {
        currentQRDataURL = canvas.toDataURL('image/png');
      }
      if (!currentQRDataURL) {
        alert('No QR code to download. Please generate one first.');
        return;
      }
    }
    
    // Create a temporary anchor to trigger download
    const link = document.createElement('a');
    link.download = 'qrcode-' + Date.now() + '.png';
    link.href = currentQRDataURL;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
  }
  
  // --- Clear handler ---
  function clearAll() {
    inputField.value = '';
    container.innerHTML = '';
    placeholderMsg.style.display = 'block';
    downloadBtn.disabled = true;
    currentQRCode = null;
    currentQRDataURL = null;
    updateCharCount();
    // Reset controls to defaults
    sizeSlider.value = 256;
    sizeValue.textContent = '256';
    darkColorInput.value = '#000000';
    lightColorInput.value = '#ffffff';
    correctionSelect.value = 'M';
  }
  
  // --- Event listeners ---
  inputField.addEventListener('input', onInputChange);
  
  sizeSlider.addEventListener('input', function() {
    sizeValue.textContent = this.value;
    onConfigChange();
  });
  
  darkColorInput.addEventListener('change', onConfigChange);
  lightColorInput.addEventListener('change', onConfigChange);
  correctionSelect.addEventListener('change', onConfigChange);
  
  downloadBtn.addEventListener('click', downloadQRCode);
  clearBtn.addEventListener('click', clearAll);
  
  // --- Initial state ---
  updateCharCount();
  placeholderMsg.style.display = 'block';
  downloadBtn.disabled = true;
</script>

How It All Works

The QR code generation pipeline follows a specific sequence. Understanding this flow helps you debug issues and extend functionality:

Best Practices for QR Code Generators

When building a QR code generator for production use, follow these guidelines to ensure reliability and a great user experience:

Extending the Generator — Advanced Features

Once the core generator works, you can extend it with features that make it truly production-ready:

Adding a Logo Overlay

Many branded QR codes include a centered logo. You can achieve this by drawing the QR code to a canvas and then compositing a logo image in the center. Here's the approach:

function addLogoOverlay(canvasElement, logoImageSrc) {
  const canvas = canvasElement;
  const ctx = canvas.getContext('2d');
  const logo = new Image();
  
  logo.onload = function() {
    // Calculate logo size — typically 20-25% of QR code size
    const logoSize = canvas.width * 0.22;
    const x = (canvas.width - logoSize) / 2;
    const y = (canvas.height - logoSize) / 2;
    
    // Draw a white background square behind the logo
    // to cover QR modules that the logo will obscure
    ctx.fillStyle = '#ffffff';
    const padding = 4;
    ctx.fillRect(
      x - padding, 
      y - padding, 
      logoSize + padding * 2, 
      logoSize + padding * 2
    );
    
    // Draw the logo centered
    ctx.drawImage(logo, x, y, logoSize, logoSize);
  };
  
  logo.src = logoImageSrc;
}

Always use high error correction (level H) when adding logos, as the logo obscures a portion of the data modules. The error correction redundancy ensures the code remains scannable despite the obstruction.

Generating High-Resolution Downloads

For print-quality QR codes, generate a separate high-resolution canvas off-screen:

function downloadHighResQR() {
  const config = getConfig();
  const highResSize = 1024;
  
  // Create an off-screen container
  const offScreenContainer = document.createElement('div');
  offScreenContainer.style.position = 'absolute';
  offScreenContainer.style.left = '-9999px';
  document.body.appendChild(offScreenContainer);
  
  const highResQR = new QRCode(offScreenContainer, {
    text: config.text,
    width: highResSize,
    height: highResSize,
    colorDark: config.colorDark,
    colorLight: config.colorLight,
    correctLevel: config.correctLevel
  });
  
  setTimeout(() => {
    const canvas = offScreenContainer.querySelector('canvas');
    if (canvas) {
      const dataURL = canvas.toDataURL('image/png', 1.0);
      const link = document.createElement('a');
      link.download = 'qrcode-highres-' + Date.now() + '.png';
      link.href = dataURL;
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
    }
    // Clean up
    document.body.removeChild(offScreenContainer);
  }, 100);
}

Conclusion

Building a QR code generator with JavaScript is a rewarding project that combines practical utility with meaningful technical learning. You've now seen how to construct a complete generator — from the HTML skeleton and CSS styling to the JavaScript logic that handles real-time generation, configuration controls, and file downloads. The qrcode.js library abstracts away the dense mathematics of QR encoding while giving you full control over the visual output and user experience.

Whether you embed this into a larger application, deploy it as a standalone tool, or use it as a foundation for more advanced features like logo overlays and high-resolution exports, the core principles remain the same: capture input, configure parameters, generate the QR matrix, render to canvas, and provide a clear path to download or share. With the best practices covered here — proper sizing, contrast management, debounced input handling, and error correction selection — your generator will produce codes that scan reliably across devices and conditions.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles