Brand colours and text that stays readable
When every merchant can pick their own header colour, the text on it has to choose its own colour too. Relative luminance does the work.
A hosted checkout often wears the merchant’s brand. On the payment pages I worked on, merchants and partners could configure their header colour, and some co-branded flows came with their own palette. The text on that header had to stay readable whatever colour arrived. A pale yellow needs dark text; a deep blue needs white.
Hard-coding white text works until the first merchant picks yellow.
Let luminance decide
The fix is to let the colour decide. The original implementation used a brightness calculation; the version here uses relative luminance, the measure behind the WCAG contrast rules, which is what I’d use today. Compare the contrast ratio of the background against both candidate text colours, and whichever ratio is higher wins.
const channel = (v: number) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);
const luminance = (r: number, g: number, b: number) =>
0.2126 * channel(r / 255) + 0.7152 * channel(g / 255) + 0.0722 * channel(b / 255);
const ratio = (a: number, b: number) => (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
Green counts for far more than blue in that formula, because our eyes are much more sensitive to it. That’s why a “bright” blue can still need white text, while a yellow of similar saturation needs black.
Do it once, as a variable
We computed the header text colour when the theme loaded and stored it as a CSS variable (--text-header). Every component that sat on the header used the variable. The theme config stayed small (just the brand colour), and the readable colour followed automatically.
Edge cases worth handling
- Middle greys. Some colours give similar contrast with both black and white. Pick the higher one, but consider nudging the brand colour slightly darker or lighter if neither reaches 4.5:1.
- Icons and borders on the header. They need 3:1 against it, so derive them from the same variable.
- Gradients and images. Check the contrast against the area the text actually sits on, not the average.