Building an Accessible Colour Palette: WCAG 2.2 Contrast Ratios and Colour-Blind Safe Verification
Build an accessible palette as a contrast matrix, not a colour pick. Verify every pair with the WCAG 2.2 formula and test for colour vision deficiency distinguishability.
The Matrix, Not the Mood Board
You stare at six colours that look beautiful side by side. Every combination of text on background fails the 4.5:1 contrast check except the black-on-white you started with. The plugin you trusted said the grey-blue passed. You tested it on a real screen at 14 pixels and the antialiased edge pixels had blended the foreground and background so thoroughly that the reported ratio was inflated by over a point and a half. Building a WCAG 2.2 compliant palette is not a colour-picking exercise. It is a matrix-building exercise. Every cell in the grid is a contrast ratio that must be verified against the relative luminance formula, not a plugin's pass/fail indicator.
Step One: Anchor the Extremes
Before you touch a mid-tone, a tint, or an accent, define the two colours that will do the most work: the lightest surface and the darkest text. These are your anchors. Every other decision hangs off them. For a typical light-mode interface, that is white or near-white for the backdrop and a near-black like #1A1A1A for the text. Do not pick pure black unless you are prepared to lose the subtlety of shadows and elevation cues. The 4.5:1 minimum for normal text is so easy to beat with a dark grey that pure black wastes your contrast budget. Now take out the calculator and compute the actual ratio. The WCAG 2.2 contrast ratio formula is (L1 + 0.05) / (L2 + 0.05), where L1 is the relative luminance of the lighter colour and L2 the darker. Relative luminance is calculated from the sRGB colour space using the coefficients R × 0.2126 + G × 0.7152 + B × 0.0722, applied after linearizing each channel with the threshold at 0.04045. Below it, divide the component by 12.92. Above it, apply ((component + 0.055) / 1.055) ^ 2.4. Do this by hand once for your anchor pair. You will get a number like 15.9:1, and you will understand the formula well enough to know exactly what your palette is capable of.
Step Two: Build the Grey Steps
With your extremes locked, build the intermediate neutral scale. These are the greys that will carry your UI component boundaries, your secondary text, your disabled states, and your focus indicators. The rule is not aesthetic. Each step must achieve at least 3:1 against its adjacent neighbour for UI component boundaries to pass the WCAG 2.2 non-text contrast requirement. A button fill next to a page backdrop, a card stroke next to the card fill, an input border next to the input field. Every one of those adjacent colour pairs needs a ratio of 3:1 or better. This is where most palettes fail. Designers pick greys by even increments of visual lightness, but the WCAG formula is not visually even. A grey at 50% luminance sits at roughly 2.4:1 against white. You need to darken it to around 40% to cross the 3:1 threshold. Build a table, a spreadsheet, or a proper contrast matrix in a tool like the EightShapes Contrast Grid. It lays out pairwise contrast ratios for every combination in your palette. Watch for the cells that fall below 3:1. Those are your component boundaries. Those are your failures. Fix them by adjusting the grey steps until every adjacency in the matrix passes.
Step Three: Add Accents One at a Time
Now the accents. Here is the discipline that separates a palette from a collection of favourite colours: you add one accent at a time, and you test it against every surface it will ever sit on, including the hover and focus states it will have. Do not assume a colour that works on white will work on your lightest grey. Take a blue accent at #0057B8. Against white, that is a 7.5:1 ratio, fine for normal text. Against your lightest grey at #F0F0F0, it drops to about 6.9:1, still fine. Against a tinted card at #E8EDF2, it drops further. You are not looking for the ratio to stay high. You are looking for it to stay above 4.5:1 for normal text, 3:1 for large text, which WCAG defines as at least 18pt or 14pt bold, and 3:1 for the focus indicator against both adjacent colours. That last one is the one everyone misses. A focus ring around a button sits between the button fill and the page surface. It must hit 3:1 against both. If your button fill is dark blue on a light backdrop, a white focus ring might just barely pass against the page but fail against the button fill. Test each accent against each backdrop in a matrix. If any cell fails, you have two choices: darken the accent or lighten the backdrop. Do not compromise on the accent's hue to make it work. There are thousands of blues, and one of them will fit.
Step Four: Simulate, Then Verify by Eye
Contrast ratio is only half the equation. A pair can pass the numbers and still be unusable for people with colour vision deficiency. The WCAG formula measures luminance difference, not hue difference. Run every colour pair through a colour blindness simulator for the three most common types: deuteranopia, protanopia, and tritanopia. Tools like Adobe Colour have a built-in Colour Blind Safe generator that shows how your palette looks under each condition. A pair that passes contrast ratio but fails distinguishability under simulation is not accessible. The classic failure is red and green. They can have nearly identical luminance and still pass a contrast check. Under deuteranopia they collapse into a muddy brown. You need at least three distinct hues, blue, orange or red, and bluish-green, to maintain distinguishability across the protanopia and deuteranopia simulations.
The Antialiased-Edge False Pass
The simulator is not the final check. The final check is your own eyes at actual rendering size. A contrast checker plugin that samples a rendered pixel where font smoothing has blended foreground and background can inflate the reported ratio by up to 1.5:1. This is the antialiased-edge-pixel false-pass failure mode. It is the reason you must not trust a plugin's pass/fail indicator on its own. Zoom out to the actual size your user will see. Look at the edges of the characters. If you can see the blend, the contrast is marginal even if the number says otherwise.
WCAG 2.2 Contrast Ratio Palette: The Exact Minimums
Here are the WCAG 2.2 contrast minimums you will be verifying against. They are not suggestions. Normal text, anything under 18pt or under 14pt bold, must hit 4.5:1. Large text, defined as at least 18pt or 14pt bold, must hit 3:1. The enhanced AAA level demands 7:1 for normal and 4.5:1 for large text. Target this for anything aimed at an older audience or a low-light reading environment. UI components and graphical objects, which includes form input borders, focus indicators, and state boundaries, must hit a separate 3:1 against adjacent colours. Note that this is distinct from text contrast. A button label might pass at 4.5:1 while the button itself fails the non-text requirement because its boundary against the page is too faint.
The Focus Indicator Rule
The focus indicator has its own additional requirement under WCAG 2.2. It must be at least 2 CSS pixels thick and change the appearance of the element so it is not just a colour swap. A focus ring that only changes from blue to darker blue fails for users who cannot perceive that hue shift. It must also alter shape, position, or weight. For text over an image, the safe practice is a scrim or overlay of at least 30% opacity black, with a gradient starting at 60% at the bottom and fading to 0%.
Colour-Blind Safe Combinations: What Contrast Misses
When you simulate for colour blindness, you are not looking for a pass or fail on a number. You are looking at whether the colours remain distinguishable as separate elements. A common mistake is to simulate, see that two colours look similar, then adjust the hue and re-test the contrast ratio. The ratio will have changed. The hue shift altered the luminance. You must iterate both variables together. The most reliable colour-blind safe combinations for UI are built on the blue/orange axis. That axis is preserved in protanopia and deuteranopia. A blue button on a white page with an orange focus ring will remain distinguishable for most colour vision deficiencies. A green button on a red error state will not. Test your palette matrix under all three simulations. For each pair, ask: can I still tell which one is the button and which one is the backdrop? If the answer is no, the pair fails, regardless of what the contrast ratio says. The simulation is a gate, not a rubber stamp.
Accessible Brand Colour System: Tokens, Not Hex Codes
This matrix-building exercise produces colours. The accessible brand colour system that will survive contact with a design team, a developer, and next year's redesign is not a page of hex codes. It is a set of semantic tokens. Name your colours by their role, not their hue: color-text-primary, color-bg-surface, color-border-interactive, color-accent-focus. A developer can implement a token named color-text-primary without knowing or caring that it resolves to #1A1A1A. When you need to darken the text for a contrast fix, you change the token's value in one place and every component in the system updates. This is the discipline of Material Design 3, which generates tonal palettes with tokens based on tone-based contrast. Never hardcode a hex code in a component stylesheet. The hex code is the output. The token is the contract.
The Single Most Common Failure
This protects you from the single most common failure in contrast management. A designer tweaks a blue from #0057B8 to #004A99 to pass a contrast check. The developer who hardcoded the old hex code in a button component never gets the update. With tokens, the update propagates.
Contrast Checker False Pass Antialiasing: The 1.5:1 Problem
Here is the specific failure that will cost you a day of rework if you do not understand it. A contrast checker plugin works by sampling a rendered pixel. On a screen, text is rendered with subpixel rendering and font smoothing. These blend the foreground and background at the edges of each glyph. A plugin that samples a pixel at the exact boundary of a glyph may sample a pixel that is a 50% blend of the text colour and the backdrop colour. That blended pixel has a luminance roughly halfway between the two. When the plugin computes the contrast ratio from that blended pixel, it reports a ratio that is lower than the text colour against the backdrop alone. The reported ratio can be inflated by up to 1.5:1. A text colour that truly sits at 4.6:1 might be reported as 6.1:1 by a sloppy plugin. The fix is not to abandon plugins. It is to verify at actual rendering size. Zoom your browser to 100% at your target breakpoint. Look at the text. If it feels thin or the edges look washed out, measure it again with a colour picker on a solid area of the glyph, not on an edge pixel. The WebAIM contrast checker is one of the more reliable tools. It lets you enter hex codes directly and does not sample rendered pixels. Nothing replaces the visual check.
Large Text Small Text Contrast Minimums: Where the Lines Fall
The distinction between large text and small text is where most contrast failures happen. The threshold is a size, not a weight. WCAG 2.2 defines large text as at least 18 point, or at least 14 point bold. These are the sizes at which the 3:1 minimum applies. For anything smaller, the minimum is 4.5:1. But point size on the web is measured in CSS pixels, and the WCAG definition assumes a standard viewing distance and a sans-serif font with a typical x-height. A typeface with a tall x-height can pass accessibility minimums at sizes where a low-x-height face fails. The visual size of the lowercase letters is what matters, not the point size of the em box. Test at the actual font size your design uses, with the actual font family. A 16-pixel lowercase letter in a font with a 0.7 x-height ratio is visually much smaller than a 16-pixel lowercase letter in a font with a 0.55 x-height ratio. When in doubt, apply the 4.5:1 minimum to everything under 18 pixels, regardless of weight. Treat the bold size threshold as a fallback for display type only.
Non-Text Contrast 3:1: Borders, Icons, and Focus Rings
Text is only half the accessibility problem. WCAG 2.2 extends the 3:1 minimum to non-text contrast, which includes UI components and graphical objects. A graphical object is anything required to understand the content that is not text: an icon, a chart line, a graph bar. The requirement is that the visual information needed to identify the component must have a 3:1 contrast ratio against the adjacent colours. For a form input, this means the border must hit 3:1 against the page surface. For an icon button, the icon must hit 3:1 against the button fill. For a focus indicator, the outline must hit 3:1 against both the element it surrounds and the page behind it. This is the rule that catches the grey-on-white border at #DDDDDD. It looks fine to a designer with 20/20 vision but measures at about 1.6:1 against white. A failure. The fix is to darken the grey to around #767676 for a 3:1 ratio on white, or to add a second visual cue like a fill or an underline. The non-text rule is also why a disabled state that uses only opacity to differentiate is a failure. If the disabled element is the same colour but at 50% opacity, it is still visually distinguishable to a sighted user. The contrast ratio of the text inside it has dropped below 4.5:1 even though the text colour itself did not change.
The Colour Palette Matrix, Completed
You have built a matrix where every cell that represents a real design combination, text on background, border on fill, focus ring on page, contains a contrast ratio that passes the WCAG 2.2 minimums applicable to it. You have tested the accents under deuteranopia, protanopia, and tritanopia simulations. You have verified the antialiased-edge-pixel false-pass failure mode by eye at actual rendering size. The matrix is your spec. The design system tokens are your contract. The final step is to document the failure cases so the next person does not have to rediscover them. The grey that looked fine but failed at 3:1. The accent blue that passed contrast but failed the deuteranopia simulation. The focus ring that disappeared on the dark surface. Write it all down, including the exact hex values and the ratio each pair achieved. Your future self, or your successor, will thank you when a stakeholder asks for a fresher colour and the whole matrix has to be re-verified.
Tools That Do the Heavy Lifting
You do not have to do the luminance math by hand for every pair. The EightShapes Contrast Grid is a spreadsheet-style tool that takes a list of hex codes and outputs a full matrix of pairwise contrast ratios. This is your primary tool for the matrix-building exercise. The Accessible Palette Generator by TPGi is similar in spirit and lets you generate a palette against a target contrast ratio. Adobe Colour's Colour Blind Safe generator shows how your palette appears under protanopia, deuteranopia, and tritanopia simulations, and includes a contrast checker. The WebAIM contrast checker is a reliable resource for a single pair check. It accepts hex codes directly.
Beware the Plugin That Lies
Watch out for the automated plugin that samples rendered pixels. It can report a false pass on an antialiased edge. The Stark plugin is popular, but its results depend on the rendering context and can be fooled by subpixel rendering. For a final verification, use a colour picker on a solid area of the glyph, not the edge, and compute the ratio manually. Or trust only a tool that takes hex codes as input.
APCA and the Future of Contrast
Before you finalise, know that WCAG 2.2's formula is not the only game in town. It is not without its critics. The Advanced Perceptual Contrast Algorithm, or APCA, is a candidate replacement that models contrast more perceptually. It accounts for font weight and size in a way WCAG's luminance formula does not. APCA recommends a minimum body-text contrast of Lc 75 for light mode and dark mode alike, with a minimum large-text contrast of Lc 60. It sets font size thresholds at 24px for body text and 36px for large text, with weight thresholds of 400 and 300 respectively. APCA is better at predicting perceived contrast. It is not yet part of WCAG 2.2, so you cannot use it to claim compliance. What you can do is use APCA as a sanity check. If a colour pair passes WCAG 4.5:1 but fails APCA Lc 75, your text is probably going to be hard to read for many users, even if it is technically compliant. For now, build to WCAG 2.2. It is the enforceable standard. Keep an eye on APCA if you want a system that will not need a complete rework when the guidelines change.
What to Do When the Palette Is Too Restrictive
There will come a moment when you have an accent colour you love, and it fails the contrast test on every surface you want to use it on. The brand team loves it. The CEO's son designed it. At 4.2:1 on white, it is a failure. You have options. None of them involve breaking the law of the matrix. First, see if the colour can be used for something other than text or essential UI. A decorative accent, a divider, a tint behind a darker text colour. A light blue at #D6E6FF can be a lovely backdrop for a dark navy text at #001F3F. That pair passes. Second, use the accent for a non-text element with a dark fill, and pair the label in a passing colour. Third, change the hue. No brand colour is so sacred that it cannot be shifted a few degrees on the hue wheel to find a version that passes contrast. The OKLCH colour space is useful here. It has a perceptual lightness axis (L), a chroma axis (C), and a hue axis (H). You can hold the hue constant while adjusting perceptual lightness until the contrast ratio passes. A colour that is too dark on white can be lightened on the L axis until it crosses the 4.5:1 threshold. This is not a compromise. It is the job.
Accessible Design Systems Go Beyond Colour
A palette built on the matrix above will get you a long way. An accessible design system is more than contrast ratios. It is also the touch target size. WCAG 2.2 requires a minimum of 24×24 CSS pixels, and WCAG 2.5.8 at AAA requires a minimum of 44×44 CSS pixels. It is the focus indicator, which must be visible and at least 2 CSS pixels thick, with a contrast ratio of 3:1 against both adjacent colours. It is the use of a scrim or overlay at a minimum of 30% opacity black behind text over images, with a gradient starting at 60% at the bottom and fading to 0%. These are not optional extras. They are part of the same WCAG 2.2 specification that defines the contrast ratios. A colour-blind safe palette that ignores them is a palette that still fails users. When you specify your system, include all of these in the documentation, not just the hex codes.
A Word on Brand Colour Psychology
You will see articles telling you that blue means trust and green means growth and that your brand colour should evoke a specific emotion. This is largely unproven when it is used as a primary decision-making framework. What is proven is legibility, contrast, and colour vision deficiency. Choose a blue because it happens to be your company's colour, not because 'blue means trust'. Choose it because it passes 4.5:1 on white and remains distinguishable under deuteranopia simulation. The emotional impact of colour is real. Red does increase heart rate. But it is a secondary consideration to the primary job of a brand colour system: making content legible and interfaces usable. If you must talk about colour psychology, frame it as an input among many. Note that the evidence base for specific emotional responses is thin and often contradictory across cultures. The evidence base for contrast ratios is not thin. It is the result of decades of research into human visual perception. Trust the numbers, not the mood boards.
The Unforeseen Failure: Subpixel Rendering and Font Smoothing
One last failure mode deserves its own section. It will bite you the moment you deploy to production. Subpixel rendering, used by most Windows browsers and some Linux environments, uses the red, green, and blue subpixels of an LCD screen to increase the perceived resolution of text. This can make text appear sharper. It also changes the colour of the edge pixels. A common result is a coloured fringe, often magenta or cyan, on the edges of dark text on a light surface. This fringing is an antialiasing artifact. It will affect a contrast checker plugin that samples those edge pixels. On a Retina display, subpixel rendering is typically disabled. Font smoothing uses greyscale antialiasing instead, which blends the foreground and background without colour fringing. The difference in reported contrast ratio between a subpixel-rendered edge and a greyscale-rendered edge can be the difference between a pass and a fail. The safe move is to verify your contrast ratios on the platform your primary audience uses. Assume that any plugin that samples rendered pixels can be fooled. Your own eyes at 100% zoom, on a solid part of the glyph, are the final arbiter.
Documentation That Saves a Rework
You have built the matrix. You have verified the ratios. You have simulated the colour blindness and checked the antialiased edges. Now put it in a document that another human being can follow. Include the anchor colours and their hex codes. Include the grey steps and the contrast ratio of each step against its neighbour. Include each accent colour, the surfaces it is approved on, and the contrast ratio for each approved pair. Include the verification steps: the URL of the contrast grid, the name of the simulator, and a note to verify by eye at the target breakpoint. Include a section on what to do when a stakeholder requests a new colour. The process is not 'pick a hex code and add it to the design file'. It is 'run this hex code through the matrix, simulate it, and only then add it to the tokens'. This documentation is what separates a design system from a collection of assets. It is the difference between a system that evolves and one that collapses the first time someone needs to add a new state.
The Cost of Getting It Wrong
The failure case is not abstract. It is a user with low vision who cannot read your text because the contrast ratio is 3.9:1, and your plugin said it passed. It is a user with deuteranopia who cannot tell the difference between the 'save' and 'cancel' buttons. They are the same hue of green and red, and the contrast ratio alone could not catch it. It is a class-action lawsuit under the ADA for a website that fails WCAG 2.2 compliance. That is a real and growing risk for companies above a certain size. More modestly, it is a customer who gives up on your checkout flow because the form labels are too faint to read in full sunlight. They go to a competitor. The cost of doing this right is an hour of calculation and a few minutes of simulation. The cost of doing it wrong is measured in lost revenue, legal exposure, and a reputation for caring about the people who use your product. The matrix is not a bureaucratic hurdle. It is the cheapest form of quality assurance you will ever implement.
If You Only Have Ten Minutes
You do not need to build the entire matrix in one sitting if you are retrofitting an existing palette. Here is the minimum viable version. Identify your lightest surface and darkest text. Confirm they hit 15:1 or better. Lock them. Take your most common secondary text colour and check it against the backdrop. If it is below 4.5:1, darken it until it passes. Take your primary action button and check the text on the button fill and the button fill against the page. Both must pass. Run those three pairs through a colour blindness simulator. That covers 80% of your real-world usage. Then book an hour in your calendar for next week to build the full matrix for the remaining colours. The ten-minute version is not a replacement for the full process. It is far better than shipping a palette that has not been tested at all. It is the difference between a known risk and an unknown one. Unknown risks are the ones that come back to haunt you.
This Guide Is Part of a System
This guide to building an accessible colour palette is itself a component of a larger system: a design system for a website, a set of tokens for a product, a brand guideline for a company. The principles here are the same ones that guide the design system's overall architecture. Semantic naming over presentational naming. Tokens over hardcoded values. Verification over assumption. When you build your own accessible brand colour system, you are not just choosing colours. You are choosing a process. The process is the same whether you are the sole designer at a small non-profit or a senior designer at a large enterprise. Define your anchors. Build your steps. Test your accents. Simulate your colour blindness. Verify by eye. Document it all. The tools change. The colours change. The process is the constant. It is the process, more than any single colour, that will make your system accessible, maintainable, and defensible when a stakeholder asks why the palette looks the way it does. You will be able to say: because it passes WCAG 2.2, and here is the matrix that proves it.
A Checklist for the Final QA
- Run every text-and-background pair through the WCAG 2.2 relative luminance formula, not a plugin's pass/fail indicator.
- Check that each step in the neutral scale hits at least 3:1 against its adjacent neighbour for UI component boundaries.
- Test each accent against every surface it will appear on, including hover and focus states.
- Simulate every pair for deuteranopia, protanopia, and tritanopia, and confirm distinguishability, not just contrast ratio.
- Verify the antialiased-edge-pixel false-pass failure mode by looking at the text at actual rendering size, not just the number.
- Confirm the focus indicator is at least 2 CSS pixels thick and hits 3:1 against both adjacent colours.
- Check that large text (≥18pt or ≥14pt bold) meets the 3:1 minimum, and normal text meets 4.5:1.
- Apply the 3:1 non-text contrast minimum to all UI components and graphical objects.
- Use a scrim of at least 30% opacity black behind text over images.
- Document the matrix, the tokens, and the failure cases so the next person does not have to rediscover them.
Frequently Asked Questions
What is the WCAG 2.2 contrast ratio for normal text?
The minimum contrast ratio for normal text is 4.5:1, where normal text is defined as anything under 18pt, or under 14pt bold.
What is the contrast ratio for large text?
Large text, defined as at least 18pt or 14pt bold, requires a minimum contrast ratio of 3:1.
What is the non-text contrast requirement?
UI components and graphical objects must have a contrast ratio of at least 3:1 against adjacent colours.
How do I calculate contrast ratio?
Use the formula (L1 + 0.05) / (L2 + 0.05), where L1 is the relative luminance of the lighter colour and L2 is the darker, calculated from sRGB components using the coefficients R × 0.2126 + G × 0.7152 + B × 0.0722.
Why does my contrast checker plugin sometimes fail?
Plugins that sample rendered pixels can be fooled by antialiasing and subpixel rendering, which blend foreground and background at glyph edges, inflating the reported ratio by up to 1.5:1.
What is the focus indicator requirement?
The focus indicator must be at least 2 CSS pixels thick, have a contrast ratio of 3:1 against both adjacent colours, and change more than just colour.
What is the minimum touch target size?
WCAG 2.2 requires a minimum of 24×24 CSS pixels, while WCAG 2.5.8 AAA requires 44×44 CSS pixels.
Meta: The One Sentence That Makes This Guide Unique
The final check is your own eyes at actual rendering size, because a contrast checker plugin that samples a rendered pixel where font smoothing has blended foreground and background can inflate the reported ratio by up to 1.5:1, and this is the antialiased-edge-pixel false-pass failure mode that you must verify by inspecting the glyph edges at 100% zoom, not by trusting the number on your screen.