QR Code With a Logo in JavaScript: Safe Zones, Coverage, and Code
How to composite a logo onto a QR code in JavaScript without breaking it: the error-correction budget, coverage maths in module units, the guard plate, canvas and SVG implementations, and how to verify the printed result.
Putting a logo in the middle of a QR code is not decoration layered on top of a working image. It is a deliberate act of destroying part of the data and relying on redundancy to survive it. That framing changes which numbers matter, and it is the reason most "add a logo" tutorials produce codes that fail on the second phone.
This article is the implementation explanation. If you want the finished tool — logo upload, a live coverage meter, print-sized PNG and SVG export, and batch labels from a CSV — it is the QR code generator with logo on this site.
The budget you are spending
A QR symbol carries redundant codewords so that damage can be repaired. The error-correction level sets how much: L restores roughly 7% of codewords, M about 15%, Q about 25%, H about 30%. Your logo spends that budget. Cover more than the level can rebuild and the code does not degrade gracefully — it stops working.
The second thing tutorials get wrong is the unit. A logo that is 25% of the code's **width** covers about 6% of its **area**, because area is the square of the edge. People read "keep the logo under 20%" and "error correction recovers 30%" and conclude they have headroom for a huge mark, when in fact a 30%-width logo already hides 9% of the symbol and the plate around it hides more.
So compute coverage as an area, and compare it against the level:
const logoSide = (logoPercent / 100) * symbolModules; // modules
const plateSide = logoSide + 2 * (platePadding / 100) * symbolModules;
const coveredArea = (plateSide / symbolModules) ** 2;
const budget = { L: 0.07, M: 0.15, Q: 0.25, H: 0.30 }[level];
const usage = coveredArea / budget; // <0.5 comfortable, <0.9 tight, >1 expect failuresA practical starting point: 18–25% of the symbol width with the plate on, at level H. That is 3–6% of the area against a 30% budget — comfortable. Past 30% width you are spending most of the margin on cosmetics, and a scratched laminate finish is enough to finish it.
Place it in module units, not pixels
The overlay must be positioned relative to the **symbol**, and the symbol's rendered image includes the quiet zone. If you write `x = (canvas.width - logoSize) / 2` with `logoSize = canvas.width * 0.22`, then changing the quiet zone silently changes how big the logo looks against the data modules — and a preview that is 22% of the canvas is not the same code as a download that is 22% of the canvas.
Do the maths in modules once, then scale:
import QRCode from "qrcode";
async function renderWithLogo(canvas, payload, logoImage, opts) {
const { level = "H", margin = 4, logoPercent = 22, platePadding = 3 } = opts;
await QRCode.toCanvas(canvas, payload, { width: canvas.width, margin, errorCorrectionLevel: level });
const symbolModules = QRCode.create(payload, { errorCorrectionLevel: level }).modules.size;
const imageModules = symbolModules + margin * 2;
const scale = canvas.width / imageModules;
const logo = (logoPercent / 100) * symbolModules * scale;
const plate = (platePadding / 100) * symbolModules * scale;
const centre = canvas.width / 2;
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#ffffff";
ctx.fillRect(centre - logo / 2 - plate, centre - logo / 2 - plate, logo + plate * 2, logo + plate * 2);
ctx.drawImage(logoImage, centre - logo / 2, centre - logo / 2, logo, logo);
}One geometry, every export size. The same numbers place the mark correctly at 320 px and at 3,000, which is exactly what a print sheet needs.
The plate matters more than the logo
A QR decoder is looking for transitions between dark and light. A logo with a mid-tone edge does not remove modules, it *softens* them, and that is worse: the error-correction machinery is designed for missing data, not ambiguous data. A solid white rectangle behind the mark converts "sort of grey" into "clearly absent", which is the case it can repair.
So keep the plate, size it generously (2–4% of the symbol on each side is normal), and give it a little rounding only if the rounded corners still leave white where the modules were. A logo with transparency and no plate is the single most common cause of "it scanned fine on my phone".
Loading the logo, and CORS
If the mark comes from a file the user picks, read it as a data URL rather than a blob URL. A data URL can be embedded in the exported SVG and stays valid after the tab closes; a blob URL is an in-memory reference that dies with the document.
const readAsDataUrl = (file) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result));
reader.onerror = reject;
reader.readAsDataURL(file);
});If you load a remote logo into a canvas instead, remember that a cross-origin image taints the canvas and `toDataURL` will throw. Set `crossOrigin = "anonymous"` and serve the asset with the right header, or accept that the export step fails on other people's CDNs. Reading the file locally avoids the whole class of problem — which is also why a purely client-side tool has fewer failure modes than one that fetches and composites server-side.
SVG: the format you want for print
PNG at 3,000 px is fine. But an SVG is one file, it stays correct at any size, and it can carry the logo as an embedded image. Because the renderer emits a `viewBox` in module units, the overlay is two extra elements inserted before `</svg>`:
async function svgWithLogo(payload, logoDataUrl, opts) {
const { margin = 4, level = "H", logoPercent = 22, platePadding = 3 } = opts;
const base = await QRCode.toString(payload, { type: "svg", margin, errorCorrectionLevel: level });
const symbolModules = QRCode.create(payload, { errorCorrectionLevel: level }).modules.size;
const imageModules = symbolModules + margin * 2;
const logo = (logoPercent / 100) * symbolModules;
const plate = (platePadding / 100) * symbolModules;
const centre = imageModules / 2;
const overlay =
`<rect x="${centre - logo / 2 - plate}" y="${centre - logo / 2 - plate}" width="${logo + plate * 2}" height="${logo + plate * 2}" fill="#ffffff"/>` +
`<image href="${logoDataUrl}" x="${centre - logo / 2}" y="${centre - logo / 2}" width="${logo}" height="${logo}" preserveAspectRatio="xMidYMid meet"/>`;
return base.replace("</svg>", overlay + "</svg>");
}One caution: the renderer writes a background path *and* a modules path. If you are extracting geometry from the SVG, grabbing the first `<path>` you find gets you the background rectangle — which composites into a solid black square and looks plausible in a thumbnail. Extract the `viewBox` and re-host the whole inner markup, or keep the two paths separate by name.
What must not be stylised
Everything in a QR symbol is load-bearing except the centre. Specifically:
- The three finder patterns (the big corner squares) are how a reader finds orientation and perspective. Never put artwork near them, and never round them into blobs.
- The timing patterns (the dashed lines between the finders) and the format information next to the top-left finder are equally structural.
- The quiet zone is not decoration; it is the margin the algorithm uses to know where the symbol ends. Four modules minimum.
- Gradients, low-contrast brand colours and module shapes other than squares all reduce the transition sharpness the decoder relies on. If you must use brand colours, check the contrast ratio — below about 4.5:1, start expecting failures.
Many codes at once
A batch run is the same code in a loop, with two changes. First, generating hundreds of symbols is synchronous CPU work: chunk it, or run it in a Web Worker if you want the UI to stay alive. Second, exporting a hundred PNGs one at a time is a bad user experience — build one vector page instead. Because the `qrcode` SVG output is a `viewBox` in modules, you can nest each symbol inside an A4-sized SVG at an exact millimetre box and hand over one printable file:
const mm = (value, dpi) => Math.round((value / 25.4) * dpi);
function sheetSvg(cells, { cellMm = 30, dpi = 300 } = {}) {
const cell = mm(cellMm, dpi);
const gap = mm(3, dpi);
const margin = mm(8, dpi);
const pageW = mm(210, dpi);
const pageH = mm(297, dpi);
const cols = Math.floor((pageW - margin * 2 + gap) / (cell + gap));
const boxes = cells.map((inner, i) => {
const x = margin + (i % cols) * (cell + gap);
const y = margin + Math.floor(i / cols) * (cell + gap);
return `<svg x="${x}" y="${y}" width="${cell}" height="${cell}" viewBox="0 0 ${size} ${size}">${inner}</svg>`;
});
return `<svg xmlns="http://www.w3.org/2000/svg" width="${pageW}" height="${pageH}" viewBox="0 0 ${pageW} ${pageH}">${boxes.join("")}</svg>`;
}Keep the cap honest. Two hundred symbols is already a second of work on a mid-range phone; the alternative is a server, which is a different product from "your Wi-Fi password never leaves this tab".
Verification, which is the actual deliverable
A logo'd code needs to be tested as a physical object, not a file:
1. Decode it in a test. Render the final composition — after the overlay — and read it back with a decoder. If you are doing this in the browser only, at minimum scan it once from the screen before you build anything else.
2. Print one at the real size on the real stock and scan it from the distance people will stand. This catches the module-edge and gloss problems no preview can show.
3. Test one iPhone and one Android. The two camera stacks behave differently around low contrast and inverted symbols, and "works on my phone" has ended more than one print run.
4. Test at the smallest size the artwork will ever appear at — the business card, not the poster.
The tool on this site does step 1 for you in CI: every payload format is encoded, painted, read back with a real decoder, and compared byte-for-byte, and the same check confirms the exported SVG's overlay is centred where the on-screen preview showed it.
When to use a tool instead of this code
If you are adding branded QR codes to a product, the code above is what you want. If you need one for a flyer this afternoon, use the with-logo generator — it has the coverage meter, the print-size arithmetic and the CSV batch path already wired up, and it never uploads your logo. For the payload formats themselves, see the QR code generator and the build guide.
Frequently Asked Questions
How big can a logo be in a QR code before it stops scanning?
Treat it as an area, not a width. A logo 25% of the symbol width covers about 6% of the symbol area, and the plate around it covers more than that. Level H restores about 30% of codewords, so 18-25% of the width is comfortable; past 30% of the width you are spending most of the recovery budget and should expect failures on damaged or small prints.
Do I need a white background behind the logo?
Yes, keep it. A decoder relies on sharp transitions between dark and light modules, and a semi-transparent logo edge turns missing data into ambiguous data, which error correction handles worse. A solid white plate converts that into clearly absent modules, which is the case it can rebuild.
Should I export a logo'd QR code as PNG or SVG?
SVG for anything printed: it is resolution-independent, and the logo can be embedded as a data URL so the file needs no companion asset. PNG is the right answer when the asset has to be a bitmap, in which case export at the print size you need — 3,000 px is 254 mm at 300 dpi.
Can I change the colours of a QR code with a logo?
You can, with limits: keep the contrast ratio above about 4.5:1, keep the modules darker than the background, leave the finder patterns and quiet zone alone, and test the printed result. Inverted light-on-dark codes are valid in the format but some phone readers still refuse them.
How do I check a QR code with a logo actually works?
Decode it rather than looking at it. Render the final composition to a bitmap, run it through a decoder such as jsQR, and compare the returned text with the intended payload byte for byte. Then print one at the real size on the real material and scan it on one iPhone and one Android at the distance people will stand.
Try the QR Code Generator With Logo
Skip the setup work and use the interactive tool to upload a logo, adjust the safe overlay size, and download a PNG directly in your browser.
Open the QR Code Generator With Logo tool