On a clean screenshot, every colour works. That is why most colour advice is wrong.
We tested a QR code at a contrast ratio of 1.36:1 - pale grey on white, almost invisible to a human - and both decoders read it perfectly from the exported PNG. Colour does not start costing you anything until the image stops being perfect. So we made it imperfect, on purpose, and measured how much abuse each colour could take.
What we did
We generated 48 QR codes with the live generator on this site: 24 colour pairs, each at error correction L and H. Every code was then normalised to 6 pixels per module - a realistic printed size - and put through a simulated bad scan: increasing defocus blur, then sensor noise on top, the way a real camera degrades in poor light.
Each severity level adds 0.35 of blur sigma and 6.0 grey levels of noise. A code “survives” a level only if it decodes on all three independent noise draws, because a single random draw is a coin flip rather than a measurement.
The score for each colour is the worst level it survives. Higher is tougher.
Finding 1: contrast predicts toughness, closely
Across all 48 codes, the correlation between contrast ratio (log scale) and the amount of degradation survived is r = 0.898. That is a tight relationship for a physical measurement, and it means contrast ratio is a genuinely useful proxy for “will this survive the real world”.
Grouped the way this site’s own contrast warning groups them:
- 4.5:1 and above (36 codes) survived to a median of level 4, up to level 6.
- 3.0:1 to 4.5:1 (2 codes) reached levels 3 and 4.
- Below 3.0:1 (10 codes) managed a median of level 2, up to level 3, and the worst never got past level 1.
A code below 3:1 takes roughly half the abuse of one above 4.5:1 before it stops reading. The thresholds the generator already warns at turn out to sit in the right place.
Every colour we tested
Error correction L, the generator’s default. “Survives to” is the worst degradation level that still decoded on all three noise draws.
| Colour pair | Contrast | Survives to | Blur at limit | OpenCV reads it |
|---|---|---|---|---|
| black on white | 21.00:1 | 6 | 2.1 | yes |
| near-black on white | 18.88:1 | 5 | 1.75 | yes |
| white on near-black | 18.88:1 | 5 | 1.75 | no |
| brand lime on brand black | 17.19:1 | 5 | 1.75 | no |
| near-black on cream | 16.96:1 | 5 | 1.75 | yes |
| dim lime on near-black | 11.25:1 | 4 | 1.4 | no |
| dark grey on white | 10.44:1 | 5 | 1.75 | yes |
| navy on white | 10.36:1 | 5 | 1.75 | yes |
| off-white on navy | 9.39:1 | 4 | 1.4 | no |
| dark brown on white | 9.37:1 | 5 | 1.75 | yes |
| navy on pale blue | 8.49:1 | 4 | 1.4 | yes |
| slate on white | 7.56:1 | 5 | 1.75 | yes |
| brown on peach | 6.92:1 | 4 | 1.4 | yes |
| deep red on white | 5.67:1 | 4 | 1.4 | yes |
| teal on white | 5.47:1 | 5 | 1.75 | yes |
| teal on pale cyan | 5.26:1 | 5 | 1.75 | yes |
| mid grey on white | 4.83:1 | 3 | 1.05 | yes |
| magenta on white | 4.54:1 | 4 | 1.4 | yes |
| red on white | 3.76:1 | 3 | 1.05 | yes |
| hot pink on white | 2.65:1 | 3 | 1.05 | yes |
| light slate on white | 2.56:1 | 2 | 0.7 | yes |
| yellow on white | 1.92:1 | 2 | 0.7 | yes |
| pale grey on white | 1.48:1 | 1 | 0.35 | yes |
| near-white on white | 1.36:1 | 1 | 0.35 | yes |
Finding 2: light-on-dark codes are a gamble on the scanner
Look down the last column. Of the 40 dark-on-light codes, OpenCV read 40. Of the 8 light-on-dark codes, it read 0.
That is not a contrast problem. White on near-black is a contrast ratio of 18.9:1, about as strong as a QR code can be, and OpenCV still refused it at full size on a perfectly clean image. Invert the picture and it decodes instantly. ZXing, which tries both polarities by default, read every one of them.
So an inverted QR code is not broken and it is not low quality. It simply depends on whether the scanner on the other end bothers to try the other polarity, and a meaningful share of them do not. If the code is going somewhere you cannot control - a poster, a package, a stranger’s phone - dark modules on a light background remains the safe choice.
Finding 3: raising error correction does not rescue a weak colour
The standard advice for a coloured or low-contrast code is to crank error correction up to H. We tested every colour pair at both L and H. Going from L to H made things slightly worse on average: -0.25 levels.
The reason is geometric rather than mysterious. Error correction does not add resilience for free - it adds data, and more data means a bigger code. Our test payload went from 29 modules to 41 when we switched from L to H. At the same printed size, those 41 modules are each smaller than the 29 were, so every module is more fragile to exactly the blur and noise the error correction was supposed to defend against. The two effects very nearly cancel.
Error correction is the right tool for damage that covers part of a code, such as a logo in the middle. It is close to useless against damage spread evenly over the whole code, which is what poor contrast and a bad camera produce.
If a colour is too weak, the fix is a better colour, a bigger print, or more light. Not level H.
Method
- Codes. Generated by the live generator on this site, exported through the normal download path, so each includes the four-module quiet zone.
- Normalisation. Every sample resized to 6 pixels per module, so the only variable is colour, not code size.
- Degradation. Gaussian defocus then additive sensor noise, in that order, because that is the order a real camera applies them. 15 severity levels, 3 independent noise seeds each.
- Primary decoder: ZXing. Chosen for the contrast ladder because it handles both polarities, so it measures colour rather than polarity.
- Compatibility decoder: OpenCV. Reported separately, which is how the inverted-code finding surfaced at all.
- Contrast ratio is the standard WCAG relative-luminance ratio, the same formula the generator uses for its own live warning.
What this does not tell you
Blur and noise are a stand-in for a bad scan, not a substitute for one. Real failures also involve uneven lighting, glare, motion, print dot gain and ink bleed, and a phone camera has autofocus and exposure control working in your favour. Treat the levels as a way to rank colours against each other, not as an absolute prediction of whether a specific code will scan.
We also held one thing fixed that matters: everything was tested at 6 pixels per module. Print bigger and a weak colour buys back some of what it lost.
Pick your colours here
The generator warns you live when a colour pair falls below these thresholds, and it checks both ends of a gradient rather than just one. Free, no sign-up, no watermark.
Keep reading
Related pages on QR Studio.
- the companion test on logo coverageHow much of the middle you can cover before it fails.
- the generator that made these codesColour and logo are the two things people change first.
- codes printed large and read at distanceContrast matters more outdoors than anywhere else.
- pastel invitation palettesThe exact colour range this test found most fragile.
- codes read in low gallery lightDim rooms are where weak contrast fails first.