On this page 10 sections
- Readable colour is a requirement, not a preference
- WCAG contrast, and what the ratios mean
- APCA, and why some teams look at it too
- Body text, UI chrome and disabled states
- Dark mode is a second palette, not an invert
- Colour blindness and charts
- A method for picking a palette and checking it
- What automated checks miss
- Key takeaways
- Frequently asked questions
I keep light and dark palettes on leej.uk, and I built the Contrast Checker and the APCA Palette Optimiser because a pair that looked fine on a calibrated monitor did not always survive a phone screen. The method below is how I pick and check a palette, not a promise that one ratio will do all of the work.
Readable colour is a requirement, not a preference
Colour is part of making a page usable, in the same way typography and focus states are. A light-grey paragraph on a white card may match a design file and still be a strain. A green-and-red chart may be obvious to you and unreadable to someone with deuteranopia. A dark theme that is only an invert of the light theme will usually produce at least one failing pair.
This is not a request to make the site ugly, or to abandon a brand colour. It is a request to give body text, controls and data enough contrast against the background they actually sit on, in both themes, at the size they are actually used. I use the same seriousness on the web that I do when I write about accessible iOS apps in SwiftUI: if a person has to struggle to read the primary content, the feature is not finished.
Brand colours often fail as text. They can still be used for a large display heading, a border, or a block of fill, with a neutral, high-contrast colour for the body. That is a design decision you should make on purpose.
WCAG contrast, and what the ratios mean
WCAG 2 measures contrast as a ratio between two colours, from 1:1 (identical) to 21:1 (black and white). For current WCAG 2.1 and 2.2 conformance, the commonly used targets are:
- Normal text, Level AA: at least 4.5:1.
- Large text, Level AA: at least 3:1. “Large” here means at least 18 point (typically 24 CSS pixels) or 14 point bold (typically 18.66 CSS pixels). Confirm the current wording in the Understanding document if your type is near the boundary.
- Normal text, Level AAA: at least 7:1, and 4.5:1 for large text.
- User interface components and graphical objects needed to understand the content, Level AA (1.4.11 Non-text Contrast): at least 3:1 against adjacent colours.
Those ratios are the conformance method for WCAG 2. If you need to claim AA, you measure with this algorithm, using the text size and weight that the user actually gets, including after zoom and after your CSS has run.
A few practical cautions:
- 4.5:1 is a minimum for AA, not a comfortable ceiling, especially for small, light-weight text, or for people reading on a poor display. If a paragraph is 14 pixels and weight 300, I would rather exceed 4.5:1 than sit on it.
- Placeholder text, disabled text and legal footnotes still need to be readable if a person is expected to read them. Disabled controls have some nuance in the guidelines; do not use that as a reason to fade important helper text until it vanishes.
- The ratio is between the text and the background it actually sits on, including a gradient, an image, or an overlay. A 4.5:1 pair on a flat swatch can fail on a photograph.
- Link colour must contrast with the background, and if you do not use another cue such as an underline, it must also be distinguishable from surrounding text. Do not rely on colour alone.
The Contrast Checker will compute a WCAG ratio for a pair. It will not see the image behind your hero heading unless you test that pairing too.
APCA, and why some teams look at it too
APCA, the Accessible Perceptual Contrast Algorithm, is a newer way of estimating how contrast is perceived, including the difference between dark text on a light background and light text on a dark background. It is part of the work around WCAG 3, and it is not how you conform to WCAG 2 today. I will not pretend a particular APCA value is a legal stand-in for 4.5:1.
It is still a useful second check, because WCAG 2’s ratio can rate some dark-mode pairs more generously, or more harshly, than they feel, and because polarity (which colour is the text) matters to readers. APCA guidance is expressed in Lc (lightness contrast) values, with different suggested floors for body text, large headings, and UI, which also depend on size and weight. Those lookup tables can be updated; use the current APCA documentation when you adopt a number, and do not treat a remembered Lc as a permanent standard.
On this site I use WCAG 2 as the ship/no-ship bar for conformance, and I use APCA, via the APCA Palette Optimiser, when I am building a palette, especially the dark one, to catch pairs that are technically close to a WCAG pass and still feel thin. If the two methods disagree, I do not pick the more flattering one. I tighten the pair until I am happy with both, or I change the type size.
Body text, UI chrome and disabled states
Different jobs need different floors, even inside one theme.
Body text and long-form reading should be the most conservative. This is where I want a comfortable WCAG margin, not a pair that only just passes at 16 pixels. If the brand colour cannot get there on the page background, the body should use a near-black or near-white of the theme, not the brand hue.
Headings can sometimes use a slightly lighter or more colourful treatment if they are large enough to meet the large-text rule, but a thin, pale heading over a busy image is still a failure. Check the actual background.
UI chrome — buttons, borders, icons, form outlines, focus rings, graph axes — must meet the non-text 3:1 requirement where the component has to be seen to be used. A 1-pixel pale border around an input on a white card is a common miss. A focus ring that is only a slight colour shift is another. If you design for small screens or for a watch-like density, the same lesson I use when designing for the Apple Watch applies: the control has to remain identifiable at a glance, not only in a large desktop mock.
Disabled states are often faded until they fail both body and non-text contrast. If the control is visible, the label should still be readable enough to be understood, or you should hide the control instead of leaving a ghost of the text. Do not rely on “disabled is meant to look faint” as a complete argument.
Visited and hover styles should not collapse to a colour that only works for one of those states. Test each.
Dark mode is a second palette, not an invert
Inverting a light palette, or swapping each colour for its complementary, will usually produce at least one of these problems:
- Mid greys that passed on white become muddy on near-black, or the reverse.
- Brand colours that were dark enough as text on cream are not light enough as text on charcoal, or they glow and vibrate.
- Borders that were dark lines on paper become invisible on a dark surface.
- Shadows and overlays that assumed a light page no longer separate layers.
- An APCA or perceptual check disagrees with a WCAG pass you had relied on, because polarity changed.
Build a second palette: backgrounds, surfaces, body text, muted text, borders, links, focus, and alerts, each tested against the surface it sits on. Then test the same components in both themes, including images and charts. A theme switch that only inverts the CSS variables without a second pass is unfinished. I would catch that in a preview the same way I would catch a layout bug in SwiftUI previews for design QA: look at both states, on purpose, before you ship.
Also test a system “increase contrast” or forced-colours mode if you can. The Forced Colours Previewer is a way to see how little of your brand survives when the user has asked the browser to impose a colour scheme. The page should still be readable, with your borders and focus styles not depending on colours that will be discarded.
Colour blindness and charts
Colour-vision deficiency is common, and it is not one condition. Deuteranopia and protanopia affect red–green distinctions; tritanopia affects blue–yellow. A red-versus-green success and error pair, or a “two-line” chart in blue and purple that collapse together, will lose information even when both lines passed a contrast check against the background.
Rules that hold up:
- Do not use colour as the only cue. On a chart, use position, pattern, line style, or a direct label. In a form, use text and an icon as well as a red border.
- Check the palette through a simulator. The Colour Blindness Simulator will show you whether two series still separate. The CVD-Safe Chart Palette is a starting set, not a guarantee: you still have to test it on the actual chart, at the actual size, on both backgrounds.
- Adjacent colours in a key or a choropleth must also contrast with each other enough to be told apart, not only with the page background.
- Print and a greyscale export are still a useful sanity check for a data graphic, even if you never print the page.
A pretty palette that fails these tests is a decoration, not a data display.
A method for picking a palette and checking it
This is the sequence I use when I add a theme, or when I am asked to “just use the brand colours”:
- Lock the backgrounds first: the page, the card, the header, in both light and dark. These are the surfaces everything else must work on.
- Choose a body text colour that is comfortable on each surface. Measure it with a WCAG checker, and look at a paragraph, not a single word in a swatch. Adjust until you have margin above 4.5:1, and until an APCA check does not look thin for that size.
- Choose muted text, links, and visited links, and test each against the same surfaces, including link-in-paragraph contrast if you are not underlining.
- Set borders, icons, and focus rings to at least the 3:1 non-text bar, on every surface they appear on, including hover and dark mode.
- Place the brand colour where it can be large or decorative, or where you have already shown it can meet the bar as text. If it cannot, do not use it for small labels.
- Build a chart palette with a colour-blind safe set, extra cues, and a check on both themes.
- Walk the real components: a form, a table, a button row, an alert, a disabled field, a hero with an image, a mobile width. Use the Contrast Checker on any pair you are unsure about, including text over a photograph.
- Only then save the tokens. A palette that exists only in a logo presentation will not survive contact with a footer of 12-pixel links.
If a pair fails, change the colour, the size, the weight, or the background. Do not change the requirement.
What automated checks miss
Automated contrast tools are necessary and incomplete. They typically miss:
- Text on images, gradients, video, or glassmorphism that was sampled from the wrong pixel.
- Text that is computed in CSS (
color-mix, transparency, a theme variable that does not apply in the test) and is not the colour in the design token file. - Overlays, sticky headers, and cookie banners that sit on top of copy.
- Placeholders,
::selection, and caret colours. - Focus rings drawn as a shadow rather than a border, which some checkers do not treat as the component boundary.
- Charts rendered in a canvas, and colours that only appear after JavaScript runs.
- Forced-colours and high-contrast operating system settings.
- The lived experience of small type, thin fonts, and a dim screen, which no ratio fully captures.
A heavy page is also harder to read on a slow, dim screen, so a performance budget and readable colour are allies, not separate polish passes. So you still have to look. Automate the tokens and the obvious text-on-surface pairs, then preview the real pages in both themes, with a colour-blindness simulation, at a realistic size. Write the checks down the same way you would write any other method meant to be cited accurately: the ratio you claim should be the one you measured. If a check cannot see the hero text, you are not done.
Key takeaways
- WCAG 2 contrast ratios are the current conformance method: 4.5:1 for normal text at AA, 3:1 for large text and for needed UI graphics. Treat 4.5:1 as a floor, especially for small type.
- APCA is a useful second, perceptual check, particularly in dark mode, but it is not a substitute for WCAG 2 conformance, and its suggested values can change.
- Build a second palette for dark mode; do not invert the light one and hope. Test body text, muted text, borders, focus and charts on the surfaces they actually use.
- Do not rely on colour alone in charts or errors. Use a colour-blind safe palette and a second cue, and check both themes.
- Automated checkers miss text on images, overlays, canvases and forced colours. Walk the real components before you freeze the tokens.
If you want a second pair of eyes on a palette that only works in the light theme, tell me about it.
Frequently asked questions
What contrast ratio does WCAG require for text?
For WCAG 2 Level AA, normal text should be at least 4.5:1 against its background, and large text at least 3:1. Level AAA raises those figures to 7:1 and 4.5:1. Large text is defined by size and weight in the WCAG Understanding documents; confirm the current definition if you are close to the threshold.
What is APCA and how is it different from WCAG contrast?
APCA is a newer model of perceived contrast that takes polarity (light-on-dark versus dark-on-light), and the size and weight of the type, into account more explicitly than WCAG 2’s ratio. It is used in work towards WCAG 3 and as a design check today. It is not the algorithm you use to claim WCAG 2 conformance.
Is 4.5:1 enough for small text?
It is the AA minimum for normal text, not always a comfortable one. Small, light-weight type, a poor screen, or a coloured background can still feel weak at 4.5:1. I prefer to give body text more margin, or to increase size and weight, rather than to sit on the line.
How do I check contrast in dark mode?
Treat it as a separate palette. Measure each text, border and control against the dark surfaces it sits on, in WCAG 2 and, if you use it, APCA. Do not assume a pair that passed on white will pass when inverted. Walk real pages, including images, charts and overlays, not only a token sheet.
How do I pick a chart palette that is colour-blind safe?
Start with a palette that has been designed to remain distinct under common colour-vision deficiencies, then add a second cue such as a pattern, a line style or a direct label. Simulate deuteranopia, protanopia and tritanopia on the actual chart, in both themes, at the size it will be seen. The CVD-Safe Chart Palette tool on this site is a starting point, not a complete test.
Written by Lee Johnson. Published 10 November 2026.