What You'll Build
A QR Code Generator that creates QR codes for URLs, plain text, WiFi networks, email, phone numbers, and SMS. Users can download the QR as a PNG image. This project introduces you to using an external library — the standard practice in real-world development.
- Six QR types: URL, Text, WiFi, Email, Phone, SMS
- Tab switching between modes
- Generate QR codes instantly as you type
- Download the QR as a PNG file
- Uses the
qrcodejslibrary from a CDN
📱 QR Code Generator
Pick a type, enter your data
What You'll Learn
- External libraries: using
qrcodejsfrom a CDN instead of writing a QR encoder yourself - Tab-based UI: same pattern as the Percentage Calculator, now with 6 panels
- Payload formats: how WiFi, email, phone, and SMS QR codes are structured
- Canvas download: converting a canvas to a PNG file the user can save
- Real-world problem solving: knowing when to use a library vs. write from scratch
Key new skill: Knowing when to use a library. QR encoding is a complex algorithm (Reed-Solomon error correction, matrix placement, masking). Writing it from scratch would take weeks. Using qrcodejs takes 3 lines. Real developers use libraries constantly.
Build the HTML with the CDN Script
The critical difference from previous projects: we load an external library. In the final index.html, you put it in the <head> or right before your script. This is a standard practice — CDNs host popular libraries so you don't have to.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>QR Code Generator</title>
</head>
<body>
<div class="card">
<h1>QR Code Generator</h1>
<p class="sub">Pick a type, enter your data</p>
<div class="tabs">
<button class="tab active" data-type="url">URL</button>
<button class="tab" data-type="text">Text</button>
<button class="tab" data-type="wifi">WiFi</button>
<button class="tab" data-type="email">Email</button>
<button class="tab" data-type="phone">Phone</button>
<button class="tab" data-type="sms">SMS</button>
</div>
<div class="panel active" id="panel-url">
<div class="field">
<label>Website URL</label>
<input type="text" id="urlInput" value="https://makuistudio.com">
</div>
</div>
<div class="panel" id="panel-text">
<div class="field">
<label>Plain Text</label>
<textarea id="textInput">Hello from MAK Studio!</textarea>
</div>
</div>
<div class="panel" id="panel-wifi">
<div class="field">
<label>Network Name (SSID)</label>
<input type="text" id="wifiSsid" placeholder="MyWiFi">
</div>
<div class="row">
<div class="field">
<label>Password</label>
<input type="text" id="wifiPass">
</div>
<div class="field">
<label>Security</label>
<select id="wifiSec">
<option value="WPA">WPA/WPA2</option>
<option value="WEP">WEP</option>
<option value="nopass">None</option>
</select>
</div>
</div>
</div>
<div class="panel" id="panel-email">
<div class="field">
<label>Email Address</label>
<input type="email" id="emailTo" placeholder="hello@example.com">
</div>
<div class="field">
<label>Subject (optional)</label>
<input type="text" id="emailSubject">
</div>
</div>
<div class="panel" id="panel-phone">
<div class="field">
<label>Phone Number</label>
<input type="tel" id="phoneNum" placeholder="+1 555 123 4567">
</div>
</div>
<div class="panel" id="panel-sms">
<div class="field">
<label>Phone Number</label>
<input type="tel" id="smsNum" placeholder="+1 555 123 4567">
</div>
<div class="field">
<label>Message (optional)</label>
<textarea id="smsMsg"></textarea>
</div>
</div>
<div class="qr-output" id="qrOutput">
<div class="qr-placeholder">QR code will appear here</div>
</div>
<button class="download-btn" id="downloadBtn" disabled>Download PNG</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<script src="script.js"></script>
</body>
</html>
About the CDN script: cdnjs.cloudflare.com is a free, reliable CDN that hosts thousands of popular JavaScript libraries. When your page loads, it fetches the library and makes it globally available as QRCode.
Style It with CSS
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f1f5f9;
padding: 20px;
color: #0f172a;
min-height: 100vh;
}
.card {
max-width: 460px;
margin: 0 auto;
background: #fff;
border-radius: 16px;
padding: 24px;
box-shadow: 0 10px 40px rgba(15,23,42,0.08);
}
h1 { font-size: 1.3rem; margin-bottom: 4px; }
.sub { color: #64748b; font-size: 0.85rem; margin-bottom: 16px; }
.tabs {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 5px;
margin-bottom: 16px;
}
.tab {
background: #f1f5f9;
border: 2px solid #e2e8f0;
color: #64748b;
padding: 8px 4px;
border-radius: 8px;
font-size: 0.75rem;
font-weight: 700;
cursor: pointer;
transition: 0.2s;
font-family: inherit;
}
.tab:hover { border-color: #cbd5e1; color: #334155; }
.tab.active {
background: #eff6ff;
border-color: #3b82f6;
color: #1d4ed8;
}
.panel { display: none; margin-bottom: 12px; }
.panel.active { display: block; }
.field { margin-bottom: 10px; }
label {
display: block;
font-size: 0.75rem;
font-weight: 600;
color: #334155;
margin-bottom: 4px;
}
input[type="text"],
input[type="email"],
input[type="tel"],
textarea,
select {
width: 100%;
padding: 10px 12px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 0.9rem;
font-family: inherit;
outline: none;
transition: 0.2s;
}
input:focus, textarea:focus, select:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59,130,246,0.15);
}
textarea { min-height: 60px; resize: vertical; }
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.qr-output {
background: #f8fafc;
border-radius: 12px;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
min-height: 240px;
margin-bottom: 12px;
}
.qr-output canvas,
.qr-output img {
max-width: 100%;
height: auto;
display: block;
border-radius: 6px;
}
.qr-placeholder {
color: #94a3b8;
font-size: 0.9rem;
text-align: center;
font-style: italic;
}
.download-btn {
width: 100%;
padding: 12px;
background: #3b82f6;
color: #fff;
border: none;
border-radius: 10px;
font-weight: 700;
font-size: 0.9rem;
cursor: pointer;
transition: 0.2s;
font-family: inherit;
}
.download-btn:hover { background: #2563eb; }
.download-btn:disabled { background: #cbd5e1; cursor: not-allowed; }
Write the JavaScript — Setup & Payload Builder
The JavaScript has three main parts: building the payload string for each QR type, generating the QR code with the library, and handling the download.
// State
var type = "url";
var tabs = document.querySelectorAll(".tab");
var qrOutput = document.getElementById("qrOutput");
var downloadBtn = document.getElementById("downloadBtn");
var lastCanvas = null;
// Tab switching
tabs.forEach(function(tab) {
tab.addEventListener("click", function() {
tabs.forEach(function(t) { t.classList.remove("active"); });
tab.classList.add("active");
type = tab.dataset.type;
document.querySelectorAll(".panel").forEach(function(p) {
p.classList.remove("active");
});
document.getElementById("panel-" + type).classList.add("active");
generate();
});
});
// ============================================================
// PAYLOAD BUILDER — the string that becomes the QR code
// ============================================================
function getPayload() {
if (type === "url") {
var url = document.getElementById("urlInput").value.trim();
if (!url) return "";
if (!/^https?:\/\//i.test(url)) url = "https://" + url;
return url;
}
if (type === "text") {
return document.getElementById("textInput").value;
}
if (type === "wifi") {
var ssid = document.getElementById("wifiSsid").value.trim();
var pass = document.getElementById("wifiPass").value;
var sec = document.getElementById("wifiSec").value;
if (!ssid) return "";
var esc = function(s) {
return s.replace(/([\\;,":])/g, "\\$1");
};
return "WIFI:T:" + sec + ";S:" + esc(ssid) + ";" +
(sec !== "nopass" ? "P:" + esc(pass) + ";" : "") + ";";
}
if (type === "email") {
var to = document.getElementById("emailTo").value.trim();
var sub = document.getElementById("emailSubject").value.trim();
if (!to) return "";
return "mailto:" + to + (sub ? "?subject=" + encodeURIComponent(sub) : "");
}
if (type === "phone") {
var num = document.getElementById("phoneNum").value.trim();
if (!num) return "";
return "tel:" + num;
}
if (type === "sms") {
var sn = document.getElementById("smsNum").value.trim();
var sm = document.getElementById("smsMsg").value.trim();
if (!sn) return "";
return "SMSTO:" + sn + (sm ? ":" + sm : "");
}
return "";
}
Why does each type have its own format? Phone cameras recognize standardized prefixes: WIFI:, mailto:, tel:, SMSTO:. When a phone scans a QR with one of these prefixes, it automatically opens the right app. This is a convention, not code — but every phone supports it.
Write the JavaScript — Generate & Download
// ============================================================
// GENERATE — call the QRCode library
// ============================================================
function generate() {
var payload = getPayload();
qrOutput.innerHTML = "";
if (!payload) {
qrOutput.innerHTML = 'QR code will appear here';
lastCanvas = null;
downloadBtn.disabled = true;
return;
}
if (typeof QRCode === "undefined") {
qrOutput.innerHTML = 'Library failed to load.';
return;
}
var container = document.createElement("div");
qrOutput.appendChild(container);
// 3 lines to make a QR code — the library does the hard work
new QRCode(container, {
text: payload,
width: 240,
height: 240,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.M
});
// Library draws asynchronously — wait, then grab the canvas
setTimeout(function() {
var canvas = container.querySelector("canvas");
var img = container.querySelector("img");
if (canvas) {
lastCanvas = canvas;
} else if (img) {
// Some browsers use img fallback — convert to canvas
var c = document.createElement("canvas");
c.width = 240;
c.height = 240;
var ctx = c.getContext("2d");
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, 240, 240);
ctx.drawImage(img, 0, 0, 240, 240);
lastCanvas = c;
}
downloadBtn.disabled = !lastCanvas;
}, 50);
}
// Live update on every input
document.querySelectorAll("input, textarea, select").forEach(function(el) {
el.addEventListener("input", generate);
el.addEventListener("change", generate);
});
// ============================================================
// DOWNLOAD — convert canvas to a PNG file
// ============================================================
downloadBtn.addEventListener("click", function() {
if (!lastCanvas) return;
var link = document.createElement("a");
link.download = "qr-code.png";
link.href = lastCanvas.toDataURL("image/png");
link.click();
});
// Initial generation on page load
generate();
Save all files and open index.html. The default URL generates a QR code. Try the other tabs — the QR updates when you type. Click Download PNG to save the image.
Understand the Big Ideas
1. Working with external libraries
The qrcodejs library provides a QRCode constructor. When you call new QRCode(element, options), it:
- Takes your text string
- Encodes it using the QR algorithm (Reed-Solomon error correction, matrix layout, masking)
- Draws the result as a canvas inside the element you provided
Writing that algorithm yourself would take weeks. Using the library takes 3 lines. This is the entire reason libraries exist — someone else already solved the hard problem, and they've shared their solution.
2. When to use a library vs. build it yourself
Use a library when: the problem is complex and well-solved (QR encoding, date formatting, charting, animations).
Build it yourself when: the problem is simple, you need to learn it, or the library is too heavy for what you need.
The BMI calculator had no dependencies — the math is simple. The QR generator uses a library because QR encoding is genuinely hard. Both decisions were correct.
3. Canvas to PNG download
The magic is canvas.toDataURL("image/png"). It reads the canvas pixels and returns a base64-encoded PNG data URI. Then:
var link = document.createElement("a");
link.download = "filename.png";
link.href = canvas.toDataURL("image/png");
link.click(); // triggers the browser's download
This same pattern downloads any canvas — screenshots, generated charts, drawings. Learn it once, use it everywhere.
4. Why setTimeout after generating?
The qrcodejs library renders the canvas asynchronously — it doesn't finish drawing the moment you call new QRCode(). So we wait 50ms with setTimeout before trying to grab the canvas for download.
Tip: 50ms is plenty for most devices. If you notice the download button not working reliably on slow phones, bump it to 100ms. It's a tiny delay users never notice.
Practice Challenges
🟢 Beginner
- Add a size selector — Small (150), Medium (240), Large (400)
- Add a color picker — let users choose dark and light colors
- Add a "Copy image" button using the Clipboard API
🟡 Intermediate
- Add error correction level — L, M, Q, H (H is best if you plan to add a logo)
- Add a vCard mode — generate a QR that saves a contact
- Add a download as SVG option in addition to PNG
🔴 Advanced
- Add a logo overlay — center a small image on the QR (with H error correction)
- Add history — remember the last 10 QR codes generated in localStorage
- Add batch generation — paste a list of URLs, download a ZIP of QRs
Compare your solution: We built the production version with size options, error correction levels, colors, and more — see the live QR Code Generator.
Full Source Code
Common Mistakes & Fixes
❌ "QRCode is not defined"
The library didn't load. Check that the CDN script is present and there are no typos in the URL. Also check DevTools Network tab — the request to cdnjs.cloudflare.com should return a 200 status.
❌ "Nothing happens when I click Download"
The canvas isn't ready yet. Make sure you're using setTimeout to wait for the library to finish drawing. And verify lastCanvas is set before clicking download.
❌ "The QR is too small or too large"
Change the width and height in the new QRCode() options object. Both must be the same value — the code assumes a square.
❌ "WiFi QR doesn't connect"
Check the escaping. If the SSID or password has a semicolon or quote, it must be escaped with a backslash. The esc() function handles this.
❌ "QR doesn't scan when I add colors"
Keep dark-on-light for reliable scanning. Light-on-dark (inverted) QR codes often fail because most scanners expect the finder pattern to be dark. Never use light text on a dark background.