QR Code Design and Customization
A standard QR code is a black-and-white grid of square modules — functional but visually unappealing. Custom QR codes with brand colors, rounded corners, and embedded logos perform better on packaging, business cards, and marketing materials. The challenge is balancing visual customization with reliable scanning. Push the design too far and scanners fail to read the code at all.
Understanding QR Code Tolerance
QR codes include built-in error correction — redundant data that allows the code to be read even when partially damaged or obscured. There are four error correction levels:
| Level | Error Correction | Data Capacity Impact | Use Case |
|---|---|---|---|
| L (Low) | 7% | Highest capacity | Minimal customization |
| M (Medium) | 15% | Moderate | Small logos, color changes |
| Q (Quartile) | 25% | Lower capacity | Medium logos, rounded modules |
| H (High) | 30% | Lowest capacity | Large logos, heavy customization |
When you embed a logo or apply aggressive visual changes, you are effectively "damaging" part of the QR code. The error correction level determines how much damage the code can sustain. Always use at least level Q when adding a centered logo, and level H for large logos or extensive styling.
Adding a Logo to the Center
The center of a QR code is the safest place for a logo because it contains the least structural data. The three finder patterns — the large squares in the corners — are what scanners lock onto first. Never obscure these.
function embedLogo(qrCanvas, logoImage, maxLogoRatio = 0.25) {
const ctx = qrCanvas.getContext('2d')
const qrSize = qrCanvas.width
// Calculate logo size (max 25% of QR width for H-level)
const logoSize = qrSize * maxLogoRatio
const logoX = (qrSize - logoSize) / 2
const logoY = (qrSize - logoSize) / 2
// Draw white background behind logo for contrast
ctx.fillStyle = '#ffffff'
ctx.fillRect(logoX - 4, logoY - 4, logoSize + 8, logoSize + 8)
// Draw the logo
ctx.drawImage(logoImage, logoX, logoY, logoSize, logoSize)
}
Rules for logo placement:
- Keep the logo under 25% of the QR code width at error correction level H (30% when using level H with caution)
- Always add a white border (quiet zone) of at least 4 module widths around the logo
- Never rotate the logo or make it partially transparent
- Test the result with multiple scanner apps — not just your phone's default camera
Changing Colors Safely
QR scanners rely on contrast between foreground modules and the background. You can change colors as long as the foreground is significantly darker than the background.
Safe color combinations:
| Foreground | Background | Contrast Ratio | Scannable |
|---|---|---|---|
| Black | White | 21:1 | Always |
| Dark blue | White | 15:1 | Always |
| Dark green | White | 10:1 | Usually |
| Navy | Light gray | 8:1 | Usually |
| Orange | White | 4:1 | Risky |
| Light blue | White | 2:1 | No |
| Yellow | White | 1:1 | No |
A contrast ratio of at least 4.5:1 (WCAG AA for normal text) is a reasonable minimum threshold. Below that, some scanners — especially older or budget phone cameras — will fail.
Gradients are possible but risky. Radial gradients that stay within the dark range work better than linear gradients that transition from dark to light across the code. If you use a gradient, test extensively.
/* Safe: dark-to-dark gradient */
.qr-foreground {
background: linear-gradient(135deg, #1a1a2e, #16213e);
}
/* Unsafe: dark-to-light gradient */
.qr-foreground-risky {
background: linear-gradient(135deg, #000000, #aaaaaa);
}
Rounding and Shaping Modules
Rounded modules (dots instead of squares) are the most popular visual customization. The key constraint: the finder patterns must remain recognizable. Many scanners use the finder patterns as anchor points and cannot identify the code geometry if these are heavily modified.
Safe customization approach:
- Round the data modules (the interior squares) to circles or rounded rectangles
- Keep the finder patterns as distinct squares or apply only mild rounding
- Maintain the timing patterns (the alternating strips between finders) in their original form
function drawRoundedModule(ctx, x, y, size, radius) {
const r = size * radius // radius as fraction of module size
ctx.beginPath()
ctx.moveTo(x + r, y)
ctx.lineTo(x + size - r, y)
ctx.arcTo(x + size, y, x + size, y + r, r)
ctx.lineTo(x + size, y + size - r)
ctx.arcTo(x + size, y + size, x + size - r, y + size, r)
ctx.lineTo(x + r, y + size)
ctx.arcTo(x, y + size, x, y + size - r, r)
ctx.lineTo(x, y + r)
ctx.arcTo(x, y, x + r, y, r)
ctx.closePath()
ctx.fill()
}
A radius of 0.3–0.45 times the module size gives a pleasant rounded appearance while keeping modules distinct. Going above 0.5 makes modules nearly circular, which can cause adjacent dots to merge in dense regions.
Testing Your Custom QR Code
Always test with real devices, not just desktop screen captures. Print your QR code at the intended size and scan it with:
- iPhone default camera (iOS 15+)
- Android Google Lens
- A dedicated QR scanner app
- A low-end phone camera (budget devices have less forgiving autofocus)
Test at the actual viewing distance — a QR code on a billboard needs larger modules than one on a business card. As a rough rule, the scanning distance in inches should be no more than 10 times the QR code width in inches.
Create customized, scannable QR codes with logos and colors using the generator at /tools/qr-code-generator, which lets you adjust error correction levels and preview how design changes affect scannability.