Colour Contrast Matrix - Check Every Pair at Once
A colour contrast matrix is a grid scoring WCAG contrast of every colour in a palette against every other. Rows are text, columns are background; each cell shows the ratio and pass/fail status. The diagonal represents a colour on itself.
Why it matters
Palettes are chosen colour by colour but used in pairs, and most accessibility failures are pairing mistakes. A brand blue that is fine on white and unreadable on the brand grey is a classic trap. Checking pairs one at a time, you will miss combinations. A matrix makes every pairing visible at once, so you can see which text-on-background choices are safe before a component ever ships.
An eight-colour palette hides 56 pairings (excluding the diagonal). The grid shows all of them.
How it works
The matrix is a table where:
- Rows represent text colours
- Columns represent background colours
- Each cell shows the contrast ratio and a pass/fail badge for AA and AAA
- The diagonal (where text colour equals background colour) always shows 1:1 contrast, which fails everything
To read the matrix, pick a row (your text colour) and scan across to find backgrounds (columns) that pass the standard you need. If you need AA for body text, you need 4.5:1 in that cell. If you need 3:1 for large text, a lower ratio passes.
The matrix reorders itself by default so the highest-contrast pairs sit in the top-left corner, making safe choices easy to spot. Risky pairs cluster toward the bottom-right.
What does not matter
The matrix assumes solid text and solid backgrounds. It cannot account for:
- Font weight and size (which affect what ratio you actually need)
- Gradients or patterns used as backgrounds
- Hover or focus states that change the colours
- Semi-transparent text (which requires compositing first)
Use the matrix as a screening tool, not the final word. If a pair passes 4.5:1 but uses a very light font weight on a mid-tone background, do a user test.
Code example
Computing contrast ratios for a matrix:
const relativeLuminance = (r, g, b) => {
// Convert to 0-1 range
const [rs, gs, bs] = [r, g, b].map(x => x / 255);
// Apply sRGB linearisation
const [rl, gl, bl] = [rs, gs, bs].map(c =>
c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
);
// Weighted sum: green has the most weight
return 0.2126 * rl + 0.7152 * gl + 0.0722 * bl;
};
const contrastRatio = (l1, l2) => {
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
};
// Example palette
const palette = [
{ name: "white", rgb: [255, 255, 255] },
{ name: "blue", rgb: [56, 189, 248] },
{ name: "grey", rgb: [128, 128, 128] },
{ name: "black", rgb: [0, 0, 0] },
];
// Build the matrix
const matrix = palette.map(textColor => {
const textLum = relativeLuminance(...textColor.rgb);
return palette.map(bgColor => {
const bgLum = relativeLuminance(...bgColor.rgb);
const ratio = contrastRatio(textLum, bgLum);
const aa = ratio >= 4.5 ? "PASS" : "FAIL";
const aaa = ratio >= 7 ? "PASS" : "FAIL";
return { ratio: ratio.toFixed(2), aa, aaa };
});
});
// matrix[0] is white text over all backgrounds
// matrix[0][3] is white text on black: highest contrast (21:1)
Wrong – ignoring the matrix and checking colours one at a time:
// Checking "is blue accessible?" in isolation
// You pass blue on white, so you assume it is safe everywhere
// But blue on grey or blue on another colour may fail
const blue = [56, 189, 248];
const white = [255, 255, 255];
const ratio = contrastRatio(
relativeLuminance(...blue),
relativeLuminance(...white)
);
// This ratio is fine; you ship. Later, a component puts blue on grey and fails
How Scalpel Color shows it
The Palette tab has a "Contrast matrix" toggle. Turn it on and the Palette view expands to show every colour against every other, ranked by contrast. Each cell is colour-coded: green for AA pass, yellow for AA miss but AAA pass, red for complete fail. The matrix updates in real-time if you tweak a colour.
Use it during palette refinement to spot risky pairs early. If you see a red cell in a pair you need to use together, nudge one colour slightly until the ratio climbs above 4.5:1.