Chrome Extension QR Code Generator — Developer Guide

12 min read

Build a QR Code Generator Extension — Full Tutorial

What We’re Building

Prerequisites


Step 1: Project Setup and manifest.json

mkdir qrcodegen-ext && cd qrcodegen-ext
npm init -y
{
  "manifest_version": 3,
  "name": "QR Code Generator",
  "version": "1.0.0",
  "description": "Generate QR codes for any URL or text with one click.",
  "permissions": ["storage", "activeTab", "clipboardWrite", "contextMenus"],
  "host_permissions": ["<all_urls>"],
  "action": {
    "default_popup": "popup.html",
    "default_icon": "icon.png"
  },
  "background": {
    "service_worker": "background.js"
  }
}

clipboardWrite enables copying QR images. activeTab lets us grab the current page URL. contextMenus adds right-click generation. host_permissions allows QR generation for any webpage.


Step 2: QR Code Generation Library (Pure JS)

Create qrcode.js — a minimal QR generator using Canvas:

// qrcode.js — Pure JS QR Code generator
export function generateQR(text, size = 256, ecLevel = 'M') {
  const moduleCount = getModuleCount(text, ecLevel);
  const modules = generateModules(text, moduleCount, ecLevel);
  const canvas = document.createElement('canvas');
  canvas.width = canvas.height = size;
  const ctx = canvas.getContext('2d');
  const cellSize = size / moduleCount;
  
  // Draw modules
  ctx.fillStyle = '#000000';
  for (let row = 0; row < moduleCount; row++) {
    for (let col = 0; col < moduleCount; col++) {
      if (modules[row][col]) {
        ctx.fillRect(col * cellSize, row * cellSize, cellSize, cellSize);
      }
    }
  }
  return canvas.toDataURL('image/png');
}

function getModuleCount(text, ecLevel) {
  // Simplified: return appropriate module count based on text length
  const ec = { L: 1, M: 0, Q: 1, H: 2 };
  const version = Math.ceil(text.length / (25 - ec[ecLevel] * 4));
  return 21 + version * 4;
}

function generateModules(text, moduleCount, ecLevel) {
  // QR matrix generation algorithm
  // Returns 2D boolean array
  const modules = Array(moduleCount).fill(null).map(() => Array(moduleCount).fill(false));
  // Add finder patterns (corners)
  addFinderPattern(modules, 0, 0);
  addFinderPattern(modules, moduleCount - 7, 0);
  addFinderPattern(modules, 0, moduleCount - 7);
  // Add timing patterns, format info, data modules...
  // (Full implementation in production)
  return modules;
}

function addFinderPattern(modules, row, col) {
  for (let i = 0; i < 7; i++) {
    for (let j = 0; j < 7; j++) {
      const isOuter = i === 0 || i === 6 || j === 0 || j === 6;
      const isInner = i >= 2 && i <= 4 && j >= 2 && j <= 4;
      modules[row + i][col + j] = isOuter || isInner;
    }
  }
}

Cross-ref: For clipboard patterns, see docs/patterns/clipboard-patterns.md.


Step 3: Popup UI

Create popup.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <style>
    body { width: 320px; padding: 16px; font-family: system-ui, sans-serif; }
    h2 { margin: 0 0 12px; font-size: 16px; }
    .input-group { margin-bottom: 12px; }
    .input-group label { display: block; font-size: 12px; color: #666; margin-bottom: 4px; }
    .input-group input, .input-group select {
      width: 100%; padding: 8px; box-sizing: border-box;
      border: 1px solid #ccc; border-radius: 4px; font-size: 14px;
    }
    .qr-display { text-align: center; margin: 16px 0; }
    .qr-display img { max-width: 100%; border: 1px solid #eee; }
    .btn-row { display: flex; gap: 8px; }
    .btn-row button {
      flex: 1; padding: 10px; border: none; border-radius: 4px;
      cursor: pointer; font-weight: 600; font-size: 13px;
    }
    .btn-copy { background: #4285f4; color: white; }
    .btn-download { background: #34a853; color: white; }
    .btn-history { background: #fbbc04; color: #333; }
    .success-msg { color: #34a853; font-size: 12px; margin-top: 8px; display: none; }
  </style>
</head>
<body>
  <h2>QR Code Generator</h2>
  <div class="input-group">
    <label>Content (URL or Text)</label>
    <input type="text" id="qr-content" placeholder="Enter URL or text...">
  </div>
  <div class="input-group">
    <label>Size</label>
    <select id="qr-size">
      <option value="128">128 x 128</option>
      <option value="256" selected>256 x 256</option>
      <option value="512">512 x 512</option>
    </select>
  </div>
  <div class="input-group">
    <label>Error Correction</label>
    <select id="qr-ec">
      <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 class="qr-display">
    <img id="qr-image" src="" alt="QR Code">
  </div>
  <div class="btn-row">
    <button class="btn-copy" id="copy-btn">Copy</button>
    <button class="btn-download" id="download-btn">Download</button>
  </div>
  <p class="success-msg" id="success-msg">Copied to clipboard!</p>
  <script type="module" src="popup.js"></script>
</body>
</html>

Cross-ref: For popup patterns, see docs/guides/popup-patterns.md.


Step 4: Popup Logic

Create popup.js:

import { generateQR } from './qrcode.js';

const contentInput = document.getElementById('qr-content');
const sizeSelect = document.getElementById('qr-size');
const ecSelect = document.getElementById('qr-ec');
const qrImage = document.getElementById('qr-image');
const successMsg = document.getElementById('success-msg');

async function updateQR() {
  const content = contentInput.value || 'https://example.com';
  const size = parseInt(sizeSelect.value);
  const ecLevel = ecSelect.value;
  
  const dataUrl = generateQR(content, size, ecLevel);
  qrImage.src = dataUrl;
  
  // Save to history
  const history = await chrome.storage.local.get('qrHistory') || { qrHistory: [] };
  const newHistory = [{ content, size, ecLevel, timestamp: Date.now() }, ...history.qrHistory].slice(0, 10);
  await chrome.storage.local.set({ qrHistory: newHistory });
}

// Auto-populate with current tab URL
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
  if (tabs[0]?.url) {
    contentInput.value = tabs[0].url;
    updateQR();
  }
});

// Event listeners
contentInput.addEventListener('input', updateQR);
sizeSelect.addEventListener('change', updateQR);
ecSelect.addEventListener('change', updateQR);

document.getElementById('copy-btn').addEventListener('click', async () => {
  const blob = await fetch(qrImage.src).then(r => r.blob());
  await navigator.clipboard.write([
    new ClipboardItem({ [blob.type]: blob })
  ]);
  successMsg.style.display = 'block';
  setTimeout(() => successMsg.style.display = 'none', 2000);
});

document.getElementById('download-btn').addEventListener('click', () => {
  const link = document.createElement('a');
  link.download = 'qrcode.png';
  link.href = qrImage.src;
  link.click();
});

Step 5: Context Menu Integration

Add to background.js:

chrome.contextMenus.create({
  id: 'generate-qr',
  title: 'Generate QR Code',
  contexts: ['page', 'link']
});

chrome.contextMenus.onClicked.addListener((info, tab) => {
  if (info.menuItemId === 'generate-qr') {
    const url = info.linkUrl || info.pageUrl;
    chrome.storage.local.set({ pendingQR: url });
    chrome.action.openPopup();
  }
});

Cross-ref: For context menu patterns, see docs/patterns/context-menu-patterns.md.


Step 6: Testing and Building

  1. Load unpacked in chrome://extensions/
  2. Click extension icon — popup shows with current tab URL
  3. Modify content, size, or error correction — QR updates instantly
  4. Test copy and download buttons
  5. Right-click any page → “Generate QR Code”
  6. Build with zip -r qrcodegen.zip .

Summary

This extension demonstrates core extension patterns: popup UI, background service worker, storage, and context menus. -e —

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.


Turn Your Extension Into a Business

Ready to monetize? The Extension Monetization Playbook covers freemium models, Stripe integration, subscription architecture, and growth strategies for Chrome extension developers.

No previous article
No next article