Most colour palettes are chosen the same way: someone picks colours that look right together, writes down six hex codes, and hands them to a developer. The problem surfaces later, when the muted grey caption turns out to be unreadable on the card it sits on, or the brand yellow makes button labels vanish.
The fix is not a stricter eye. It is a change in what you are checking. As a Nottingham design studio working across brand, web and print, we built the accessible colour palette generator to solve this properly, and the thing that made it work was giving up on checking colours at all.
Colours are not accessible. Pairs are.
Ask whether #2E5AAC is accessible and there is no answer. On white it clears body text comfortably. On a dark navy page it disappears. The colour did not change; the background did.
Every contrast rule in WCAG is written as a relationship between a foreground and the background behind it. So the useful question is never "are these colours accessible" but "which combinations does my interface actually draw, and do those pass?" A card headline on a surface. A label on a button fill. A border against the page.
That list is shorter than you might expect, and it is the whole job. Get those accessible colour combinations right and there is nothing left to fail.
What the contrast numbers actually mean
Three thresholds cover almost everything. WCAG 2.2 puts them in Success Criteria 1.4.3, 1.4.6 and 1.4.11:
| Ratio | Applies to | Level |
|---|---|---|
| 4.5:1 | Body text and anything under 18.5px, or under 14px bold | AA |
| 3:1 | Large text, plus interface components and graphical boundaries | AA |
| 7:1 | Body text at the enhanced level | AAA |
Two details catch people out. Large text gets an easier ride because size does some of the work that contrast otherwise has to do, so a heading can carry a colour a paragraph cannot. And the 3:1 requirement for interface boundaries does not tighten at AAA, because WCAG has no enhanced non-text contrast criterion. Scaling it to 4.5 would be inventing a rule, so our tool leaves it alone.
The ratios themselves come from relative luminance, which is a calculation rather than a judgement. That matters more than it sounds, and it is what the next section is built on.
Give every colour a job
The roles a working palette needs
Accessible brand colours are easier to reason about when each one has a defined job. These are the roles our tool solves for, and the same set covers most interfaces.
-
Primary and on primary
Your brand colour as a button or panel fill, plus the dedicated colour of the label sitting on it. Two roles, because the label has its own contrast obligation.
-
Background and surface
The page itself and the cards that sit slightly proud of it. Every foreground in the palette gets checked against both.
-
Text and muted text
Body copy and the quieter secondary line. Muted text is where most palettes quietly fail, because it is chosen by eye.
-
Border and accent
Dividers, input outlines and the secondary brand colour. Borders are graphical boundaries, so they answer to 3:1, not 4.5:1.
Keeping the brand colour you already have
The usual advice for accessible brand colours is to lighten or darken them until they pass. That is a reasonable last resort and a terrible first move: it treats the one colour the business actually owns as the variable, and the white page nobody chose as fixed.
Invert it. Pin the brand colour, then solve everything else around it. Backgrounds are outputs too. If a saturated brand colour cannot carry dark text and cannot sit on a light page, the honest answer is often a dark interface, which is a design decision you can make on purpose rather than a compromise you discover at build.
Some roles carry no obligation at all. A secondary colour that only ever appears as a decorative fill never has text on it, so it can be exactly what the brand guidelines say without being dragged toward a contrast target.
Why checking every colour against every other colour fails
Our first version of the tool scored a full grid: every colour against every other colour. It felt rigorous. It was actually broken, and not in a subtle way.
Trace two of the requirements. If body text must be readable on your primary colour, primary has to be light enough to carry dark text. If primary must also be visible as a button on the page background, it has to be dark enough to stand out against it. Add a rule that primary and accent must contrast with each other and they are simultaneously squeezed into the same narrow band of lightness and pushed apart. No set of colours satisfies all of it.
Because contrast depends only on relative luminance, that is not a matter of opinion. You can search the space. We did, exhaustively, and the best any palette could achieve under the old model was two failures at AA and three at AAA. Zero was unreachable. The "auto-generate" button could never have succeeded no matter how good the solver got.
The fix was deletion, not a cleverer algorithm. Nothing in a real interface draws primary-coloured text on an accent-coloured fill, so that pairing was never worth scoring. Button labels get their own role instead, which is how Material and Spectrum handle it, and how Adobe Leonardo and InclusiveColors model contrast generally: always a foreground against a background, never brand against brand.
Now every tile in the grid maps to something a browser renders, zero is provably reachable, and a verification script runs the real solver hundreds of times on every build to keep it that way. It is a small lesson with a wide application: an extra constraint that sounds stricter can quietly make the goal impossible rather than harder.
What the search found
The old model could never reach zero
- 2 Best possible issues at AA under the full matrix
- 3 Best possible issues at AAA under the full matrix
- 600 Solver runs in our verification script
Screens and print pull in different directions
If the identity only lives online, you have one more choice worth making. APCA scores light-on-dark differently from dark-on-light, which reflects how displays are actually read and which WCAG 2 cannot express. It is the better model for interfaces.
It is also modelled on self-illuminated screens, which means it has nothing sensible to say about ink on paper. In our tool APCA and print-gamut mode switch each other off for that reason.
Print pulls the other way. A four-colour press cannot reproduce every colour a screen can show, so an accent that glows on a monitor can come back from the printer looking dull and slightly off. If the brand needs stationery, packaging or printed marketing, restrict the palette to the ISO Coated gamut before solving rather than discovering the gap on a proof.
Build one, then hand it over
Work in this order and it takes minutes:
- Pin what you own. Drop in the brand colour, or two if you have a primary and an accent.
- Choose the target. AA for most projects. AAA when readability matters more than range.
- Say where it has to work. Turn on print gamut if the colours will ever be printed.
- Solve the rest. Let the tool fill backgrounds, text, muted text and borders around what you pinned.
- Read the grid, not the swatches. Every tile is a pairing your interface renders. Zero failures means zero.
- Export CSS variables and hand a developer roles rather than a mood board.
Try the accessible colour palette generator
Work through the steps above in the tool: pin your brand colour, choose AA or AAA, toggle print gamut if you need it, then read the pair grid for zero failures.
Common questions about accessible colour palettes
Colour is the part of accessibility that gets decided earliest and changed last, which is why it is worth getting right at the palette stage rather than during a remediation sprint. The questions below come up most often when we take clients through it as part of a website design project.
Accessible colour palette FAQs
What is the difference between a colour contrast checker and an accessible colour palette generator?
A colour contrast checker answers one question at a time: does this foreground pass on this background? Useful, but it leaves you to fix the failure yourself, usually by nudging hex values until the warning disappears. A generator solves the whole palette at once, so the colours it hands back already clear every pairing together. Use a checker to audit; use a generator to build.
Should I target AA or AAA?
AA is the level referenced by most procurement and public sector requirements, and it is a sensible default for a marketing site. AAA is worth targeting when your audience skews older, when text is read in bright or variable light, or when the content is something people must be able to read rather than choose to. AAA is harder on brand colour: at 7:1 there is a band of mid lightness that nothing can sit on.
Can I keep my existing brand colour?
Yes. Pin it, and let everything else be solved around it. That sometimes means the page goes dark rather than white, which is a better outcome than mangling a brand colour to fit a background nobody actually chose.
What is APCA and should I be using it instead of WCAG?
APCA is a newer contrast model that scores light text on dark differently from dark text on light, which WCAG 2 cannot express. It is a genuinely better model of how displays are read. It is also for self-illuminated screens only, so it never applies to print, and WCAG 2 remains the referenced standard in most compliance contexts. Our tool offers both, and switching to APCA turns print gamut off.
Will these colours print correctly?
Only if you ask for it. Plenty of screen-safe colours sit outside what a four-colour press can reproduce, and a vivid on-screen accent can come back from the printer looking flat. If the brand needs to work on paper as well as a page, restrict the palette to the ISO Coated gamut before you solve it, not afterwards.
Does an accessible palette make my site WCAG compliant?
No. Colour contrast is one criterion among many. Keyboard navigation, focus states, headings, alt text, form labels and motion all sit outside a palette. A solved palette removes a whole class of failures at the source, which is worth doing early, but it is a starting point rather than a certificate.