Custom QR Code Colors: Which Combinations Still Scan?

Everyone wants a QR code in their brand colours. Some of those codes work and some quietly do not. We tested 20 colour pairs to see where the line is.

By QRotto · Published · 9 min read

The short answer

Use a dark code on a light background, and keep the contrast at 4 to 1 or better. Every pair that met both conditions was read by every decoder we tried. Pairs that did not meet them were read by some scanners and not others, and one pair was read by none.

Almost any hue works as the code colour. Blue, red, green, purple, teal and pink all scanned. What matters is how much darker the code is than its background, not which colour it is.

If you would rather skip the reading, our custom QR code generator decodes your styled code on every change and warns you when contrast drops below 4 to 1.

How we tested

We generated one QR code, a 29 by 29 grid holding a web address, at error correction level M. We then drew it 20 times in different foreground and background colours at the same size, and tried to decode each image three ways:

For each pair we also worked out the contrast ratio using the standard web accessibility formula, where 1 to 1 means identical brightness and 21 to 1 is black on white.

Software decoders, not phones. Phone cameras use their own scanning code, and they will not all agree with ours. This test is a clean digital render with no print, no glare and no camera blur. Treat it as a map of where the risk is, then check your own code on real phones.

The results, all 20

A grid of 20 QR codes in different colour pairs. Each is labelled with its contrast ratio and how many of three decoder settings could read it. Black, navy, blue, red, green, purple, teal and pink on white, black on yellow and black on light grey read in 3 of 3. Orange, light blue, yellow and light grey on white, white on black, white on navy, yellow on black, blue on red and mid grey on light grey read in 1 of 3. Red on green read in 0 of 3.
Ten pairs read in all three decoder settings. Nine depended on the decoder. One, red on green, read in none.
Colour pairContrastOpenCVjsQR defaultjsQR, inverted too
Black on white21 : 1ReadsReadsReads
Navy on white16.11 : 1ReadsReadsReads
Blue on white5.03 : 1ReadsReadsReads
Red on white4.98 : 1ReadsReadsReads
Green on white5.13 : 1ReadsReadsReads
Orange on white3.08 : 1ReadsFailsFails
Purple on white9.39 : 1ReadsReadsReads
Teal on white4.32 : 1ReadsReadsReads
Pink on white3.76 : 1ReadsReadsReads
Light blue on white2.21 : 1ReadsFailsFails
Yellow on white1.4 : 1ReadsFailsFails
Light grey on white1.88 : 1ReadsFailsFails
White on black21 : 1FailsFailsReads
White on navy16.43 : 1FailsFailsReads
Yellow on black15.05 : 1FailsFailsReads
Black on yellow15.05 : 1ReadsReadsReads
Black on light grey15.91 : 1ReadsReadsReads
Red on green1.03 : 1FailsFailsFails
Blue on red1.15 : 1ReadsFailsFails
Mid grey on light grey1.85 : 1ReadsFailsFails

Where the contrast line sits

Look only at the dark-on-light pairs. Pink on white, at 3.76 to 1, was read by every decoder, and so was everything with more contrast than that: teal, red, blue, green, purple, navy and black.

Orange on white, at 3.08 to 1, was not. OpenCV read it and both jsQR settings failed. So for this code, the line sits somewhere between 3.08 and 3.76.

That is one code, and the exact line will move a little with the code's size and density. It is why our designer warns below 4 to 1 rather than at the lowest figure that happened to work. A margin is worth having, because printing can take some contrast away.

In the designer we saw the same story. Blue on white scanned cleanly. Orange on white scanned with a warning that contrast was only 3.1 to 1. Yellow on white and light grey on white were flagged as not scanning, with contrasts of 1.4 and 1.9 to 1.

Scanners disagree near the edge

The most useful thing in the table is how differently the decoders behaved on the weak pairs.

OpenCV read yellow on white at 1.4 to 1, and mid grey on light grey at 1.85 to 1. Both jsQR settings failed them. Blue on red, at 1.15 to 1, split the same way.

If you test a pale colour on one phone and it works, that tells you about that phone. It does not tell you about the one belonging to the person scanning your poster. Nine of our 20 pairs sat in exactly this uncertain middle.

One pair sat outside it. Red on green, at 1.03 to 1, was read by nobody. To the eye red and green look very different, but they are nearly the same brightness, and a scanner works from brightness.

Light on dark is a different problem

Three pairs had a lot of contrast and still failed: white on black, white on navy and yellow on black. Their contrast ratios are 21, 16.43 and 15.05 to 1.

The reason is not contrast. A standard QR code is dark modules on a light background, and these three are the other way round. OpenCV and default jsQR both failed all three. They only read when we explicitly told jsQR to try inverted images as well.

Compare yellow on black with black on yellow. The same two colours at the same 15.05 to 1 ratio, swapped. One read in all three settings and the other in just one. Which colour is darker matters more than which colours you chose.

Some scanners do handle inverted codes and some do not, and a printed code cannot tell you which kind it will meet. If you want a dark design, keep the code dark and make the background light, or put a light panel behind the code on the dark design.

What to do with your brand colour

  1. Make the code the dark colour and the background the light one. If your brand colour is pale, use it for the background or frame, and draw the code in a darker shade of it.
  2. Measure the contrast. Aim for 4 to 1 or more. For a code that will be scanned from a distance or printed on rough stock, aim well above that.
  3. Test with more than one scanner. Use a plain QR scanner such as ours, and at least one phone camera.
  4. Keep a plain version in reserve. A black on white file costs nothing and is the one that always works.

On gradients: a gradient code changes colour across its width, so its contrast is set by its lightest end rather than its average. We did not test gradients in this round, so that is reasoning, not a measurement. Check the pale end against the background.

What this test cannot tell you

We would rather be clear than impressive about this.

Common questions

Can a QR code be any colour?

Almost any hue works for the code, provided it is dark enough against its background. Blue, red, green, purple, teal and pink all scanned in our test. What fails is low contrast and light on dark.

Does the background colour matter?

Yes, as much as the code colour, because contrast is the gap between the two. A pale background with a dark code is the safest arrangement.

Can I use a white QR code on a black background?

Some scanners read it and some do not. In our test, white on black failed in OpenCV and in default jsQR, and read only when we asked jsQR to try inverted images. If it matters that everyone can scan it, use dark on light.

Is yellow a bad colour for a QR code?

Yellow as the code colour on white was poor: 1.4 to 1, and it read in only one of three settings. Yellow as the background with a black code was fine and read in all three.

What contrast ratio should I aim for?

4 to 1 or more as a minimum. In our test everything above 3.76 to 1 on a light background read everywhere, and the pair at 3.08 did not. More is better for print.

Can I use a transparent background?

A transparent background takes on whatever sits behind the code, so the contrast is decided by that, not by your colour choice. Use it only over a plain, light surface, and test the final result.

How can I check my own colours?

Our custom QR code generator decodes the code you are looking at after every change and reports whether it still reads, and what the contrast is. Our scanner reads a saved image back as text, which is a second opinion.

QRotto

QRotto builds free browser based QR code generators and writes the guides that go with them. Every generator is verified by decoding the codes it produces and confirming the output matches the input exactly. See our editorial standards.