If you just need one, use our free QR code generator.
Why this matters
When you use an online QR generator, you are submitting your input to someone else's server. That server logs requests. It may retain what you encoded. The operator may sell it. A breach may expose it.
What people encode into QR codes is often not public information. Internal URLs. Wi-Fi passwords. Contact details. Account recovery links. Two-factor backup codes. None of that needs to leave your device to become a QR code. The format is a mathematical transformation. It runs in milliseconds, entirely in a browser tab, with zero network activity required.
A server adds nothing to QR generation except a log entry somewhere.
What the live Blackout implementation does
- Runs entirely in the browser. Text is converted into QR modules locally using qr-kazuhiko.js.
- Uses bundled local scripts. No CDN fetch is required for the generator to work.
- Renders to canvas locally. The QR code is drawn in-browser and downloaded as PNG or JPG directly from the canvas element.
- Keeps styling local too. Foreground, background, gradient, module shape, and corner eye styling all happen client-side. Named presets (Blackout, Emerald, Electric, Monero, Midnight, Violet, Terminal, Crimson, Amber, Gold, Rose Gold) are applied without any network call.
- Applies scan-safety checks in-browser. Unsafe color combinations are adjusted locally to stay readable.
- JPG export composites locally. When downloading as JPG, the canvas is composited onto a white background in-browser before encoding. No upload involved.
Minimal implementation
The example below mirrors the current Blackout implementation direction more closely than the older template. It still stays simple: one HTML file, local scripts, local canvas rendering, and no server.
This version uses a bundled QR engine and a small render function, rather than outsourcing generation to a remote API or CDN.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>QR Generator</title>
<style>
body {
background: #111;
color: #ddd;
font-family: sans-serif;
max-width: 460px;
margin: 3rem auto;
padding: 1rem;
}
input {
width: 100%;
padding: .65rem;
background: #1a1a1a;
border: 1px solid #333;
color: #ddd;
border-radius: 8px;
font-size: 1rem;
box-sizing: border-box;
}
button {
margin-top: .7rem;
padding: .6rem 1rem;
background: #f3eee2;
color: #050505;
border: 0;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
}
#output {
margin-top: 1.5rem;
text-align: center;
}
canvas {
border-radius: 14px;
box-shadow: 0 18px 42px rgba(0,0,0,0.42);
}
</style>
<script src="./qr-kazuhiko.js"></script>
</head>
<body>
<h2>QR Generator</h2>
<p>Local only. Nothing leaves this device.</p>
<input type="text" id="qr-input" value="https://www.blackoutvpn.io" />
<input type="color" id="fg" value="#f3eee2">
<input type="color" id="bg" value="#050505">
<button onclick="generate()">Generate</button>
<button id="dl-btn" onclick="downloadQr()" disabled>Download PNG</button>
<button onclick="setFormat('png')">PNG</button>
<button onclick="setFormat('jpg')">JPG</button>
<div id="output"></div>
<script>
var SCALE = 12;
var QUIET = 4;
var currentCanvas = null;
var downloadFormat = 'png';
function utf8ByteString(str) {
return unescape(encodeURIComponent(str));
}
function buildQr(text) {
var api = window.BlackoutQRCode;
var qr = new api.QRCode(0, api.QRErrorCorrectLevel.M);
qr.addData(utf8ByteString(text));
qr.make();
return qr;
}
function renderCanvas(qr, fg, bg) {
var modules = qr.getModuleCount();
var total = (modules + QUIET * 2) * SCALE;
var canvas = document.createElement('canvas');
canvas.width = total;
canvas.height = total;
var ctx = canvas.getContext('2d');
ctx.fillStyle = bg;
ctx.fillRect(0, 0, total, total);
ctx.fillStyle = fg;
for (var row = 0; row < modules; row++) {
for (var col = 0; col < modules; col++) {
if (!qr.isDark(row, col)) continue;
ctx.fillRect((col + QUIET) * SCALE, (row + QUIET) * SCALE, SCALE, SCALE);
}
}
return canvas;
}
function generate() {
var text = document.getElementById('qr-input').value.trim();
if (!text) return;
var fg = document.getElementById('fg').value;
var bg = document.getElementById('bg').value;
var out = document.getElementById('output');
out.innerHTML = '';
currentCanvas = renderCanvas(buildQr(text), fg, bg);
out.appendChild(currentCanvas);
document.getElementById('dl-btn').disabled = false;
}
function setFormat(fmt) {
downloadFormat = fmt;
document.getElementById('dl-btn').textContent = 'Download ' + fmt.toUpperCase();
}
function downloadQr() {
if (!currentCanvas) return;
var a = document.createElement('a');
if (downloadFormat === 'jpg') {
var flat = document.createElement('canvas');
flat.width = currentCanvas.width;
flat.height = currentCanvas.height;
var ctx = flat.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, flat.width, flat.height);
ctx.drawImage(currentCanvas, 0, 0);
a.download = 'qrcode.jpg';
a.href = flat.toDataURL('image/jpeg', 0.92);
} else {
a.download = 'qrcode.png';
a.href = currentCanvas.toDataURL('image/png');
}
a.click();
}
document.getElementById('qr-input').addEventListener('keydown', function(e) {
if (e.key === 'Enter') generate();
});
generate();
</script>
</body>
</html>
Download the template Starter file using qr-kazuhiko.js. Includes foreground/background color pickers, PNG and JPG download, and live re-render on color change. No build step required.
Download templateHow to verify it
Before trusting any QR tool with sensitive data, verify it yourself. This takes under two minutes.
- Open the page in your browser. Open DevTools with
F12, or right-click anywhere and choose Inspect. - Go to the Network tab. Clear any existing entries so you are starting from zero.
- Type something into the input and generate a QR code.
- Watch the Network tab. A clean local generator produces no new requests. Nothing leaves the browser.
- Optional: open the Sources tab and inspect the page source. Confirm there are no external script tags, analytics snippets, or tracking pixels.
If you see outbound requests fire when you generate a code, the tool is sending your input somewhere. Stop using it for anything sensitive.
What breaks the privacy model
Common patterns that compromise otherwise client-side tools:
- API-based generation. The browser sends your text to a server endpoint. The QR image is rendered server-side and returned. Your input is logged in the process.
- CDN script loading. Loading a library from an external CDN tells that CDN your IP address and the page context it was loaded from.
- Analytics. Many tools that generate locally still attach usage tracking.
- GET parameter inputs. If your text is passed as a query string, it lands in logs all over the request path.
- Built-in link shortening. Any feature that wraps your URL in a short link is sending it to a third party.
If a tool can run locally, run it locally. Every external dependency you accept on behalf of a user is a trust decision you made for them. Remove the ones that add nothing.
