scalpel@labs: ~/glossary/color-contrast-matrix.mdx5 sections

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.

extension: Scalpel Colorupdated: 2026-08-14read_time: 2 min
less color-contrast-matrix.mdx

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.

Sources