UI Components Medium
QR Code
In-browser QR code generator — type a URL or text and get an instant scannable QR code with a download button.
Open in Lab
MCP
canvas vanilla-js
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: Inter, system-ui, sans-serif;
background: #050910;
color: #f2f6ff;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.demo {
width: 100%;
max-width: 440px;
}
.demo-title {
font-size: 1.5rem;
font-weight: 800;
margin-bottom: 0.375rem;
}
.demo-sub {
color: #475569;
font-size: 0.875rem;
margin-bottom: 2rem;
}
/* ── Card ── */
.qr-card {
background: #0a1120;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 16px;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
/* ── Input ── */
.input-label {
display: block;
font-size: 0.75rem;
font-weight: 600;
color: #64748b;
margin-bottom: 0.5rem;
letter-spacing: 0.04em;
}
.input-row {
display: flex;
gap: 0.5rem;
}
.qr-input {
flex: 1;
padding: 0.625rem 0.875rem;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.04);
color: #f2f6ff;
font-family: inherit;
font-size: 0.875rem;
outline: none;
transition: border-color 0.15s;
min-width: 0;
}
.qr-input::placeholder {
color: #334155;
}
.qr-input:focus {
border-color: #3b82f6;
}
.btn-generate {
padding: 0.625rem 1.125rem;
border-radius: 8px;
border: none;
background: #3b82f6;
color: #fff;
font-family: inherit;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
transition: background 0.15s, transform 0.1s;
}
.btn-generate:hover {
background: #2563eb;
}
.btn-generate:active {
transform: scale(0.97);
}
/* ── Size pills ── */
.size-row {
display: flex;
align-items: center;
gap: 0.75rem;
}
.size-label {
font-size: 0.75rem;
font-weight: 600;
color: #475569;
flex-shrink: 0;
}
.size-pills {
display: flex;
gap: 0.375rem;
}
.size-pill {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 28px;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: transparent;
color: #64748b;
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
transition: border-color 0.15s, color 0.15s, background 0.15s;
user-select: none;
}
.size-pill input[type="radio"] {
display: none;
}
.size-pill:has(input:checked),
.size-pill--active {
border-color: #3b82f6;
color: #60a5fa;
background: rgba(59, 130, 246, 0.1);
}
.size-pill:hover:not(:has(input:checked)) {
border-color: rgba(255, 255, 255, 0.2);
color: #94a3b8;
}
/* ── QR display ── */
.qr-display {
display: flex;
align-items: center;
justify-content: center;
min-height: 220px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.02);
border: 1px dashed rgba(255, 255, 255, 0.07);
position: relative;
overflow: hidden;
}
.qr-placeholder {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
color: #334155;
font-size: 0.8rem;
}
/* ── Generated QR image ── */
.qr-img {
border-radius: 8px;
image-rendering: pixelated;
display: block;
/* White padding so QR codes have the required quiet zone */
background: #fff;
padding: 8px;
}
/* ── Spinner ── */
.qr-spinner {
display: flex;
align-items: center;
justify-content: center;
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid rgba(255, 255, 255, 0.1);
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* ── Error ── */
.qr-error {
font-size: 0.8rem;
color: #f87171;
text-align: center;
padding: 0.5rem;
}
/* ── Actions ── */
.actions {
display: flex;
gap: 0.5rem;
}
.btn-action {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
padding: 0.6rem 1rem;
border-radius: 8px;
border: none;
background: #3b82f6;
color: #fff;
font-family: inherit;
font-size: 0.825rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
}
.btn-action:hover {
background: #2563eb;
}
.btn-action--secondary {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #94a3b8;
}
.btn-action--secondary:hover {
background: rgba(255, 255, 255, 0.1);
color: #f2f6ff;
}
/* ── Utility ── */
.hidden {
display: none !important;
}(function () {
"use strict";
const input = document.getElementById("qr-input");
const btnGen = document.getElementById("btn-generate");
const btnDl = document.getElementById("btn-download");
const btnCopy = document.getElementById("btn-copy");
const img = document.getElementById("qr-img");
const spinner = document.getElementById("qr-spinner");
const errorEl = document.getElementById("qr-error");
const placeholder = document.getElementById("qr-placeholder");
const actions = document.getElementById("qr-actions");
let currentUrl = "";
let debounceTimer = null;
const QR_ENDPOINTS = [
(text, size) =>
"https://chart.googleapis.com/chart" +
"?cht=qr" +
"&chs=" +
size +
"x" +
size +
"&chld=M|1" +
"&chl=" +
encodeURIComponent(text),
(text, size) =>
"https://api.qrserver.com/v1/create-qr-code/" +
"?size=" +
size +
"x" +
size +
"&data=" +
encodeURIComponent(text),
(text, size) =>
"https://quickchart.io/qr" +
"?size=" +
size +
"&margin=1" +
"&text=" +
encodeURIComponent(text),
];
/* ── Helpers ── */
function getSize() {
const checked = document.querySelector('input[name="qr-size"]:checked');
return checked ? parseInt(checked.value, 10) : 200;
}
function buildQrUrls(text, size) {
return QR_ENDPOINTS.map((buildUrl) => buildUrl(text, size));
}
function showState(state) {
placeholder.classList.toggle("hidden", state !== "empty");
spinner.classList.toggle("hidden", state !== "loading");
img.classList.toggle("hidden", state !== "ready");
errorEl.classList.toggle("hidden", state !== "error");
actions.style.display = state === "ready" ? "flex" : "none";
}
function setError(msg) {
errorEl.textContent = msg;
showState("error");
}
function loadWithFallback(urls, size, index) {
if (index >= urls.length) {
setError("Could not load QR code. Check your connection and try again.");
return;
}
const url = urls[index];
const probe = new Image();
probe.onload = function () {
currentUrl = url;
img.src = url;
img.width = size;
img.height = size;
showState("ready");
};
probe.onerror = function () {
loadWithFallback(urls, size, index + 1);
};
probe.src = url;
}
/* ── Generate ── */
function generate() {
const text = input.value.trim();
if (!text) {
currentUrl = "";
showState("empty");
return;
}
const size = getSize();
const urls = buildQrUrls(text, size);
showState("loading");
loadWithFallback(urls, size, 0);
}
/* ── Download ── */
function download() {
if (!currentUrl) return;
const a = document.createElement("a");
a.href = currentUrl;
a.download = "qr-code.png";
a.target = "_blank";
a.rel = "noopener";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
/* ── Copy URL ── */
function copyUrl() {
if (!currentUrl) return;
if (navigator.clipboard) {
navigator.clipboard.writeText(currentUrl).then(() => flash(btnCopy, "Copied!"));
} else {
// Fallback
const ta = document.createElement("textarea");
ta.value = currentUrl;
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
document.body.removeChild(ta);
flash(btnCopy, "Copied!");
}
}
function flash(btn, label) {
const original = btn.textContent;
btn.textContent = label;
setTimeout(() => {
btn.textContent = original;
}, 1800);
}
/* ── Size change → regenerate if QR shown ── */
document.querySelectorAll('input[name="qr-size"]').forEach((radio) => {
radio.addEventListener("change", () => {
// Update active pill styling
document
.querySelectorAll(".size-pill")
.forEach((p) => p.classList.remove("size-pill--active"));
radio.closest(".size-pill").classList.add("size-pill--active");
if (!img.classList.contains("hidden")) generate();
});
});
/* ── Debounced input → auto-generate ── */
input.addEventListener("input", () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(generate, 600);
});
/* ── Enter key ── */
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
clearTimeout(debounceTimer);
generate();
}
});
/* ── Button events ── */
btnGen.addEventListener("click", () => {
clearTimeout(debounceTimer);
generate();
});
btnDl.addEventListener("click", download);
btnCopy.addEventListener("click", copyUrl);
/* ── Initial render ── */
generate();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>QR Code Generator</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<h1 class="demo-title">QR Code</h1>
<p class="demo-sub">Generate a scannable QR code from any URL or text.</p>
<div class="qr-card">
<!-- Input area -->
<div class="input-group">
<label class="input-label" for="qr-input">URL or text</label>
<div class="input-row">
<input
id="qr-input"
class="qr-input"
type="url"
placeholder="https://example.com"
value="https://stealthis.dev"
autocomplete="off"
spellcheck="false"
/>
<button class="btn-generate" id="btn-generate" type="button" aria-label="Generate QR code">
Generate
</button>
</div>
</div>
<!-- Size selector -->
<div class="size-row" role="group" aria-label="QR code size">
<span class="size-label">Size</span>
<div class="size-pills">
<label class="size-pill">
<input type="radio" name="qr-size" value="150" /> S
</label>
<label class="size-pill size-pill--active">
<input type="radio" name="qr-size" value="200" checked /> M
</label>
<label class="size-pill">
<input type="radio" name="qr-size" value="280" /> L
</label>
</div>
</div>
<!-- QR display -->
<div class="qr-display" id="qr-display" aria-live="polite" aria-label="QR code preview">
<div class="qr-placeholder" id="qr-placeholder" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
<rect x="3" y="14" width="7" height="7"/>
<rect x="5" y="5" width="3" height="3"/><rect x="16" y="5" width="3" height="3"/>
<rect x="5" y="16" width="3" height="3"/>
<path d="M14 14h3v3h-3zM17 17h3v3h-3zM14 17h3M17 14h3"/>
</svg>
<p>Click Generate to preview</p>
</div>
<img
id="qr-img"
class="qr-img hidden"
alt="Generated QR code"
width="200"
height="200"
/>
<div class="qr-spinner hidden" id="qr-spinner" aria-label="Generating…">
<div class="spinner"></div>
</div>
<p class="qr-error hidden" id="qr-error" role="alert"></p>
</div>
<!-- Actions -->
<div class="actions" id="qr-actions" style="display:none">
<button class="btn-action" id="btn-download" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
Download PNG
</button>
<button class="btn-action btn-action--secondary" id="btn-copy" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
Copy URL
</button>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>QR Code Generator
Generates a scannable QR code from any URL or text string using a resilient multi-provider image strategy.
Features
- Instant preview — QR code updates as you type (debounced)
- Download — saves the image as
qr-code.png - Error handling — shows a friendly message if the API is unavailable
- Copy URL — copies the shareable QR image URL to the clipboard
Implementation
Tries multiple QR endpoints in sequence (chart.googleapis.com, api.qrserver.com, quickchart.io) and renders the first successful response as a PNG image. Download is triggered through a temporary <a> element, which avoids CORS-related fetch failures in browser sandboxes.