QR Code Design and Customization

May 28, 20266 min read

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:

LevelError CorrectionData Capacity ImpactUse Case
L (Low)7%Highest capacityMinimal customization
M (Medium)15%ModerateSmall logos, color changes
Q (Quartile)25%Lower capacityMedium logos, rounded modules
H (High)30%Lowest capacityLarge 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:

ForegroundBackgroundContrast RatioScannable
BlackWhite21:1Always
Dark blueWhite15:1Always
Dark greenWhite10:1Usually
NavyLight gray8:1Usually
OrangeWhite4:1Risky
Light blueWhite2:1No
YellowWhite1:1No

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:

  1. Round the data modules (the interior squares) to circles or rounded rectangles
  2. Keep the finder patterns as distinct squares or apply only mild rounding
  3. 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.