How to check a whole palette for contrast at once
Checking two colors is easy. Checking a palette of eight is not, because every color can end up next to every other one. The fix is a grid: compute the contrast ratio for every pair, then read it for the combinations your design uses.
This guide explains the idea, which pairs deserve attention, and how to build the grid yourself. The thresholds come from WCAG 2.2, covered in the contrast explainer.
Checked against the W3C’s WCAG 2.2 Understanding documents on November 12, 2026.
Why a grid
WCAG contrast is symmetric: black on white and white on black give the same ratio. So a palette of n colors has n × (n - 1) / 2 distinct pairs. Four colors make 6 pairs, six make 15, eight make 28, and twelve make 66. Checking those one at a time is how mistakes slip through.
A grid lays the pairs out as rows and columns. Put the text color in the rows and the background in the columns, and each cell holds one ratio. Read down a row to see how a single color performs on every background.
Which pairs matter
Most pairs in a palette never touch. Give each color a role first, then check the pairs your design actually creates:
- Body and heading text on each surface: 4.5:1 for normal text, 3:1 if it is large (18 point, or 14 point bold).
- Link and accent colors on the page background: usually text, so 4.5:1.
- Button labels on the button fill: text again.
- Borders, icons, chart lines, and focus rings on the surface behind them: 3:1 under criterion 1.4.11.
- Placeholder and secondary text on every surface they appear on.
Skip pairs that never sit side by side, such as two decorative accents on opposite ends of a page. Do not skip dark mode: it is a second palette with its own grid.
Build the grid yourself
This script prints the ratio for each pair in a small palette, using the WCAG formula. It runs as written in Node.
const lin = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
const lum = ([r, g, b]) => 0.2126 * lin(r / 255) + 0.7152 * lin(g / 255) + 0.0722 * lin(b / 255);
const ratio = (a, b) => {
const [hi, lo] = [lum(a), lum(b)].sort((x, y) => y - x);
return (hi + 0.05) / (lo + 0.05);
};
const palette = { ink: [27, 27, 31], paper: [255, 255, 255], brand: [109, 0, 255] };
for (const [nameA, a] of Object.entries(palette)) {
for (const [nameB, b] of Object.entries(palette)) {
if (nameA < nameB) console.log(`${nameA} on ${nameB}: ${ratio(a, b).toFixed(2)}`);
}
}The output is ink on paper 17.17, brand on ink 2.53, and brand on paper 6.79. Reading it: ink text on paper is excellent, brand purple text on paper passes AA, and brand purple on the ink color fails even the 3:1 minimum, so those two must not be used together for text or icons.
Reading the results
Sort each cell into a band: 7:1 or more passes AAA for normal text, 4.5:1 to 7:1 passes AA, 3:1 to 4.5:1 passes only for large text and non-text elements, and under 3:1 fails for everything. Then scan the rows for your text colors first.
Fixing a failing pair
Change lightness, not hue. Darken the text or lighten the background in small steps until the ratio clears the line, then look at the result. A light and dark variant of the same hue usually keeps the brand feel. If a color fails against several backgrounds, it probably needs a different role rather than a different shade.
In Opalite
In a palette’s accessibility view, Opalite’s Contrast tool shows exactly this grid: the row is the text color and the column is the background. You can switch the measure between WCAG and APCA, and tap any cell to open the contrast checker for that pair. A summary line counts how many pairs pass AA for normal text. For very large palettes it checks the first group of colors and says so. The grid is a convenience, not a requirement.
The short version
- Give every color a role. Label each color as text, surface, accent, border, or decoration, so you know which pairs the design actually uses.
- Compute the ratio for every pair. Use the WCAG contrast formula in a script or a tool to fill a grid with text colors in rows and backgrounds in columns.
- Judge each pair against its role. Require 4.5:1 for normal text, 3:1 for large text, and 3:1 for icons, borders, chart lines, and focus rings.
- Fix failures with lightness. Lighten or darken one color of a failing pair in small steps until it passes, keeping the hue.
- Repeat for dark mode. Run the same grid on the dark palette, since it has its own pairs.
Quick answers
- How many pairs are in a palette?
- n × (n - 1) / 2 for n colors, because WCAG contrast is the same in both directions. Eight colors make 28 pairs.
- Does it matter which color is the text?
- Not for WCAG 2, which gives the same ratio either way. It does matter for APCA, a different measure that depends on which color is the text.
- Should I check dark mode separately?
- Yes. A dark palette has different colors, so it has different pairs and its own results.
Want these checks built into your palette?
Opalite checks contrast, simulates color blindness, and exports palettes to code on iPhone, iPad, Mac, and more. Free to start.
Opalite is an independent app from Molargik Software. WCAG is published by the W3C, which is not affiliated with and does not endorse Opalite.