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:
- No third-party dependency at runtime: Once the page loads, everything happens locally in the browser. No API calls, no latency, no usage limits.
- Instant feedback: Users see the QR code update in real time as they type, creating a smooth, responsive interface.
- Full customization: You control colors, size, error correction levels, and branding — things most free online generators don't allow.
- Offline capability: A static page with embedded JS can generate QR codes even without an internet connection after the initial load.
- Learning value: QR code generation involves working with canvas elements, binary data encoding, and client-side image manipulation — all core front-end skills.
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:
- Data encoding: The input string is analyzed to determine the optimal encoding mode — numeric, alphanumeric, byte, or kanji. The library selects the most compact representation automatically.
- Error correction: Reed-Solomon error correction codewords are computed and appended. The level you choose (L, M, Q, H) determines how many codewords are added — higher levels mean more redundancy but also more modules.
- Module placement: The encoded data bits are arranged into a matrix following the QR code specification. Finder patterns (the three distinctive squares in the corners), timing patterns, and alignment patterns are placed first, then data modules fill the remaining space.
- Mask application: A mask pattern is XORed across the data region to balance dark and light modules, preventing problematic patterns that could confuse scanners. The library tries all eight masks and picks the one with the lowest penalty score.
- Canvas rendering: The final matrix is drawn pixel by pixel onto an HTML canvas element. Each module becomes a square of
width / (moduleCount + 2 * margin)pixels. - Data URL extraction: For download functionality, the canvas is converted to a PNG data URL via
canvas.toDataURL('image/png'), which produces a base64-encoded string that can be saved as a file.
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:
- Always set a minimum size: QR codes smaller than 128px may be difficult to scan on modern phones with high-resolution cameras that expect larger codes. The sweet spot for on-screen QR codes is between 200px and 400px.
- Choose error correction wisely: Use level M (15%) for general-purpose codes displayed on screen. Use level H (30%) only when you plan to overlay a logo or expect the code to be partially obscured. Higher correction levels pack more modules, requiring larger display sizes for reliable scanning.
- Provide adequate contrast: The foreground and background colors must have a high contrast ratio. Avoid light colors on light backgrounds. Test with actual scanning — what looks visible to the eye may not scan well if the luminance difference is too low.
- Add a quiet zone: The QR code specification requires a white margin (quiet zone) of at least 4 modules around the entire code. The
qrcode.jslibrary adds this automatically, but if you apply CSS borders or padding, ensure they don't encroach on this space. - Validate input length: QR codes have data capacity limits. A version 40 QR code with level L correction can hold up to 7,089 numeric characters, 4,296 alphanumeric characters, or 2,953 bytes. For URLs, keep them under 2,000 characters to stay within reasonable version sizes.
- Debounce input events: Regenerating a QR code on every keystroke can cause flickering and performance issues. Use a debounce of 250-350ms to let users type smoothly before the regeneration kicks in.
- Offer PNG download at high resolution: When implementing download, consider generating a larger off-screen canvas (e.g., 1024px) for the download file while keeping the on-screen preview smaller. This ensures the downloaded QR code prints clearly.
- Test across different scanners: Different QR scanning apps have varying capabilities. Test your generated codes with Google Lens, Apple's Camera app, and dedicated QR scanners to ensure broad compatibility.
- Handle edge cases: Test with empty strings, extremely long strings, special Unicode characters, emoji, and URLs with query parameters. The library handles most cases, but verify behavior with your specific data patterns.
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.