A scanner sees light and dark, not color
The first thing a decoder does with a camera frame is binarize it: every pixel is sorted into one of two buckets — module (dark) or background (light) — using a brightness threshold. Hue is thrown away in that step. What survives is how bright each color is, not what color it is. So the question that decides whether a code scans is not “do these two colors look different?” but “is one clearly brighter than the other?”
This is why two colors that are obviously distinct to a human eye can collapse into a single shade of gray for a camera. Red text on a green background is the classic trap: the hues are opposites, but their luminance can be nearly identical, so after binarization the modules and the gaps between them turn the same gray and the grid disappears. The same effect is what makes those color pairs hard for color-blind readers — the eye and the camera are failing for the same underlying reason.
The number that matters: contrast ratio
To put a figure on “clearly brighter,” this site uses the same WCAG luminance math it uses for accessible text. Each color is converted to a relative luminance value between 0 (black) and 1 (white), and the two are compared as a ratio:
(L_lighter + 0.05) / (L_darker + 0.05)
That gives a number from 1:1 (no contrast at all) up to 21:1 (pure black on pure white). For a QR code the practical targets are:
- ≥ 3:1 — the minimum to expect a reliable scan in good light. Below this the code is gambling on the camera and conditions.
- ≥ 4.5:1 — the recommended target. It buys headroom for cheap cameras, dim rooms, glossy printing, glare, and the slight color shift that ink and screens introduce.
Because luminance is non-linear and weighted toward green, you can’t eyeball the ratio — a saturated yellow is far brighter than a saturated blue even though both feel “vivid.” Run the actual colors through a calculator. The live check on the QR code generator scores this for you on every change, and you can test arbitrary foreground/background pairs with our sibling color contrast checker.
Why inverted (light-on-dark) codes fail
A white code on a black background can have a perfect 21:1 contrast ratio and still refuse to scan, which surprises people. The contrast is fine; the polarity is wrong. The QR specification expects dark modules on a light background, and most camera apps and decoders are tuned for exactly that convention. The position-detection squares in three corners — the patterns the scanner hunts for first to lock onto the grid — are designed as dark-on-light. Invert them and many decoders simply never find the code to begin with.
Some modern scanners do handle inverted codes, but plenty don’t, and you can’t predict which phone a stranger will point at your poster. Treat dark-on-light as a hard rule, not a style preference. If you want a dark aesthetic, put the code in a light tile or panel rather than inverting the modules themselves.
Safe and risky color pairs
You can absolutely use color — the trick is to keep a strong luminance gap. A reliable recipe is a dark, low-luminance foreground (navy, deep maroon, charcoal, forest green, dark purple) on a white or very light background. These read almost like black-on-white to a camera because their luminance is low, while still looking like a brand color to a person.
- Safe: dark navy on white, deep green on cream, charcoal on pale gray — high luminance gap, correct polarity.
- Risky: red on green, blue on purple, mid-gray on mid-gray, orange on yellow — different hues, similar luminance, low ratio.
- Avoid: any light-on-dark (inverted) scheme, and any pairing that scores under 3:1.
Gradients are a special case: a gradient on the modules is fine as long as its lightest point still clears the ratio against the background, because the lightest module is the one most likely to be misread as background. Always score the worst case, not the average.
Contrast is necessary, not sufficient
Good contrast is one of several things a code needs, and it can’t rescue other mistakes. You still need the full 4-module quiet zone around the symbol so the scanner can isolate the grid, enough error-correction headroom for any logo, and a print size large enough that individual modules don’t blur together. Contrast and polarity are the two failures this article fixes; the rest are covered in why won’t my QR code scan.
The honest test is empirical: this site doesn’t just compute a ratio, it renders your styled code and runs a real in-browser decode against it. If the decode fails, the verdict fails — no matter how good the numbers look. Style boldly, then let the check confirm a machine can still read it.
Related
References
This tool’s QR generation and scannability checks are grounded in the following standards and primary sources.
- Understanding SC 1.4.3: Contrast (Minimum) — W3C WAI — the luminance-contrast ratio used for the scan check
- Color.js — Lea Verou & Chris Lilley (MIT) — contrast + luminance math
- Point for Setting the Module Size — DENSO WAVE — quiet zone, module size & scanning distance
Spotted an error? Let us know — reader corrections are the best review this site gets.