Accessible Color Systems Guide 2026
An accessible color system is a set of named color tokens plus a list of tested foreground and background pairs. Text pairs must reach 4.5:1 contrast (3:1 for large text) under WCAG 2.2 success criterion 1.4.3, and control boundaries, focus rings and chart marks must reach 3:1 against adjacent colors under 1.4.11. You ship the pairs, not loose swatches.
That definition changes how a palette gets built. A moodboard asks "do these colors look good together?" A color system asks "which of these colors may sit on which, at what size, in which state, and in which theme?" The rest of this guide works through that second question with measured numbers, using a zinc-style neutral ramp and a blue brand color as the running example. Every ratio quoted below was computed with the WCAG relative luminance formula, and you can reproduce any of them in the site's contrast checker.
If you need the background on hue, saturation, harmony and perceptual color spaces first, the color theory guide for web designers covers that ground. This page assumes you already have colors you like and need them to survive production.
What the WCAG color rules actually require
Designers often carry around a simplified version of the contrast rules. The simplified version causes real mistakes, so here are the criteria as W3C's Understanding documents describe them.
| Criterion | Level | Applies to | Minimum ratio |
|---|---|---|---|
| 1.4.1 Use of Color | A | Any information, action prompt or distinguishing cue | Color may not be the only visual means |
| 1.4.3 Contrast (Minimum) | AA | Text and images of text | 4.5:1, or 3:1 for large-scale text |
| 1.4.6 Contrast (Enhanced) | AAA | Text and images of text | 7:1, or 4.5:1 for large-scale text |
| 1.4.11 Non-text Contrast | AA | Visual information needed to identify UI components and their states, and graphic parts needed to understand content | 3:1 against adjacent colors |
| 2.4.13 Focus Appearance | AAA | Keyboard focus indicators | Area of at least a 2 CSS px perimeter, 3:1 between focused and unfocused pixels |
Four details in those documents trip up teams again and again.
Large text has a precise meaning. Large-scale text is at least 18 point, or 14 point bold. The Understanding document for 1.4.3 converts this at 1pt = 1.333px, so the thresholds are roughly 24px regular and 18.5px bold. A 20px semibold heading is not large text for contrast purposes, even though it looks big in a mockup.
Ratios are not rounded. W3C states that a computed 4.499:1 does not meet 4.5:1, and the 1.4.11 document says the same about 2.999:1 against 3:1. This matters in practice: a mid gray of #777777 on white computes to 4.48:1 and fails, while #767676 computes to 4.54:1 and passes. One step on a hex value decides the result.
Some text is exempt. Text inside an inactive (disabled) component, purely decorative text, and text that is part of a logo or brand name have no contrast requirement under 1.4.3. The logo exemption covers the brand mark, not the brand color used elsewhere. A low-contrast logo chosen as a design decision rather than required by brand guidelines is not exempt, according to the 1.4.11 guidance.
Non-text contrast is measured against adjacent colors. For a text input, the relevant comparison is usually the input's border against the page background outside it, not against the white fill inside it. Hover styles do not need 3:1, because the pointer itself signals hover, but a hover style must not reduce the contrast of other states such as focus.
The contrast formula, worked once by hand
Contrast checkers hide the arithmetic, which makes it hard to reason about why a pair fails. Working one pair by hand shows where the numbers come from. The formula in the 1.4.3 Understanding document has two steps.
Step 1: relative luminance. Divide each 8-bit channel by 255. If the result is 0.04045 or less, divide it by 12.92. Otherwise compute ((value + 0.055) / 1.055) raised to the power 2.4. Then weight the channels: L = 0.2126 R + 0.7152 G + 0.0722 B.
Step 2: ratio. Contrast = (L1 + 0.05) / (L2 + 0.05), where L1 is the lighter color. The scale runs from 1:1 to 21:1.
Worked example: muted text #52525B on a page background of #FAFAFA.
| Value | Red | Green | Blue |
|---|---|---|---|
| Hex channel | 52 (82) | 52 (82) | 5B (91) |
| Divided by 255 | 0.3216 | 0.3216 | 0.3569 |
| Linearized | 0.0844 | 0.0844 | 0.1046 |
Weighted sum: 0.2126 x 0.0844 + 0.7152 x 0.0844 + 0.0722 x 0.1046 = 0.0858. The background #FAFAFA linearizes to 0.9560 on every channel, so its luminance is 0.9560. The ratio is (0.9560 + 0.05) / (0.0858 + 0.05) = 1.006 / 0.1358 = 7.41:1. That pair clears AA and AAA for body text.
Two lessons fall out of the arithmetic. Green carries over 70% of the luminance weight, so shifting a color's green channel moves its contrast far more than shifting blue. And the +0.05 term means that near-black colors all land close together: very dark grays differ little in ratio against white, while small changes in mid-tones swing the result by a full point.
Build the neutral ramp first
Most interface pixels are neutral. Text, borders, cards, inputs, table rows and dividers all come from the gray ramp, and an accent color cannot rescue a ramp with weak steps. The table below uses zinc-style values and states what each step may do, with ratios measured against the page background (#FAFAFA) and the raised card surface (#F4F4F5).
| Token | Hex | On #FAFAFA | On #F4F4F5 | Permitted role |
|---|---|---|---|---|
| neutral-950 | #0A0A0B | 18.96:1 | about 18:1 | Body text, headings |
| neutral-800 | #26262B | 14.43:1 | about 13.7:1 | Secondary text |
| neutral-600 | #52525B | 7.41:1 | 7.03:1 | Muted text, captions, metadata |
| neutral-500 | #71717A | about 4.6:1 | 4.40:1 | Muted text on page background only |
| neutral-400 | #A1A1AA | 2.46:1 | lower | Disabled text and decorative icons only |
| neutral-200 | #E4E4E7 | about 1.2:1 | lower | Decorative dividers only |
The neutral-500 row is the instructive one. On a white page it measures 4.83:1 and passes. Move the same caption onto a card with an #F4F4F5 fill and it drops to 4.40:1, which fails 1.4.3. The color did not change; its background did. That is why a system documents pairs, and why "muted text is neutral-500" is not a safe rule on its own.
The neutral-200 row shows the other common failure. A light divider at about 1.27:1 against white is fine when it only separates two blocks of content, because decorative lines have no requirement. Use the same value as the outline of a text field and the field's boundary becomes the only thing identifying it as an input, which brings in 1.4.11 and its 3:1 floor. A value near #8E8E96 measures 3.25:1 against white and works as an input border. Many systems keep two border tokens for exactly this reason: border-subtle for decoration and border-control for anything a user must find and operate.
Turn swatches into an approved-pairs matrix
The single most useful handoff artifact is a matrix of approved pairs. It answers the question every designer and developer asks mid-task: can this text go on that surface?
| Foreground | Background | Measured ratio | Approved for |
|---|---|---|---|
| White | Blue 600 #2563EB | 5.17:1 | Button labels, any text size |
| White | Blue 500 #3B82F6 | 3.68:1 | Large text and icons only |
| Blue 600 #2563EB | Card #F4F4F5 | 4.70:1 | Links on cards |
| Blue 700 #1D4ED8 | Info tint #DBEAFE | 5.49:1 | Text on info banners |
| White | Red 600 #DC2626 | 4.83:1 | Destructive button labels |
| White | Green 600 #16A34A | 3.30:1 | Large text and icons only |
| White | Green 700 #15803D | 5.02:1 | Success button labels |
| White | Amber 600 #D97706 | 3.19:1 | Large text and icons only |
| White | Amber 700 #B45309 | 5.02:1 | Warning button labels |
| Neutral #111111 | Amber 500 #F59E0B | 8.79:1 | Dark text on bright amber |
The pattern in this palette is worth noticing: at the 600 step, blue and red usually pass with white text, while green and amber need to go one step darker, or switch to dark text. A semantic system that uses the 600 step for every hue will quietly ship failing success and warning buttons. Test each hue separately rather than assuming one step number works across the set.
Semantic tints follow the same logic. The four light-mode tint pairs below each measure well above 4.5:1, so they can carry body-size text in alerts and badges:
| Semantic role | Tint background | Text color | Ratio |
|---|---|---|---|
| Success | #DCFCE7 | #166534 | 6.49:1 |
| Warning | #FEF3C7 | #92400E | 6.37:1 |
| Danger | #FEE2E2 | #991B1B | 6.80:1 |
| Info | #DBEAFE | #1E40AF | 7.15:1 |
Color alone is never the message
Success criterion 1.4.1 is Level A, the baseline tier, and it is broader than charts. It says color may not be the only visual means of conveying information, indicating an action, prompting a response or distinguishing an element. Interfaces break it in quieter ways than a red-green chart:
- a required field marked only by a red label;
- a form error shown only by turning the border red;
- an active tab that differs from inactive tabs only in hue;
- an inline link that is the same weight as the text around it and differs only in color;
- a status column where green, amber and red dots carry the whole meaning.
The fix is redundancy. Pair each color with a second cue: text ("Approved", "Pending", "Rejected"), an icon with a distinct shape, an underline, a border weight change, or a pattern. An icon that carries the status on its own also needs a text alternative, while one sitting beside a matching label stays silent; the guide to when an image should have empty alt text sorts icons, logos and charts into those two cases. For inline links, W3C lists technique G183 as sufficient when links are distinguished by color alone: the link text must reach at least 3:1 against the surrounding static text, on top of the link and body text each meeting 4.5:1 against the background. Finding a hue that satisfies all three ratios at once is fiddly, which is why underlining links in body copy is the simpler default.
A useful self-check: export the screen in grayscale. Anything that becomes ambiguous was relying on hue. Chart palettes need extra care here, and the chart color accessibility checklist walks through ordering series colors, direct labels and pattern fills for data graphics specifically.
Component states: where most failures hide
A palette can pass every swatch test and still fail in components, because components stack colors: a label on a fill, a fill on a surface, a ring around both. Test each state of each component, not each color.
Buttons
For each variant (primary, secondary, outline, destructive, ghost), record the default, hover, active, focus, disabled and loading states. The rules differ by part:
- The label is text. It needs 4.5:1 against the button fill, or 3:1 if it is large-scale text. An outline button's label is still text, so it also needs 4.5:1, measured against the surface behind the button.
- The boundary is non-text. If the button's shape is what identifies it as a control (an outline button, or a filled button whose fill is close to the page color), the boundary needs 3:1 against the adjacent background under 1.4.11.
- Disabled is exempt but should still be legible. Inactive components have no contrast requirement. That is a permission, not a goal. A disabled button whose label is unreadable leaves users unsure what the action is or why it is unavailable. A reasonable house rule is to keep disabled labels near 3:1 anyway and to pair the disabled state with helper text explaining what is missing.
Text inputs
Inputs carry four colors: the border, the fill, the text, and the label above them. Check the border against the page (3:1), the typed text against the fill (4.5:1), the label against the page (4.5:1) and the helper or error text against the page (4.5:1). Placeholder text is still text with a 4.5:1 requirement if it carries information, which is one more reason to keep instructions in a visible label rather than the placeholder.
Error states need a non-color cue as well as a color change: an icon plus a sentence such as "Enter a date after today", connected to the field. The red border can fail 1.4.11 against a dark page while the message still makes the state clear.
Selection and toggles
Checkboxes, radio buttons, switches and selected tabs encode state visually. The 1.4.11 guidance asks that the visual information identifying the state reaches 3:1, so a switch whose on and off positions differ only by a light blue versus light gray track fails even if both tracks are visible. A thumb that moves position plus a fill that changes from #A1A1AA to #2563EB gives two cues and enough contrast.
Focus indicators that pass both levels
Focus is where the WCAG 2.2 numbering confuses people. Three criteria are involved:
- 2.4.7 Focus Visible (AA) requires that keyboard focus is visible at all.
- 2.4.11 Focus Not Obscured (Minimum) (AA), new in WCAG 2.2, requires that the focused component is not entirely hidden by author content such as sticky headers or cookie banners. Partial covering passes at AA; keeping the component fully clear is the AAA criterion 2.4.12.
- 2.4.13 Focus Appearance (AAA), also new in 2.2, sets the size and contrast bar: an area at least as large as a 2 CSS pixel thick perimeter of the unfocused component, with at least 3:1 contrast between the same pixels in the focused and unfocused states.
The 1.4.11 document adds the AA-level expectation that a custom focus indicator must contrast with whatever is adjacent to it. An indicator drawn outside the component is measured against the page; one drawn inside is measured against the component's own colors.
A practical spec that satisfies all of this for most designs:
| Property | Value | Reason |
|---|---|---|
| Style | Solid outline, 2px | Meets the 2 CSS px perimeter size test |
| Offset | 2px outside the component | Keeps the ring from merging into a same-colored button |
| Color, light theme | Blue 600 #2563EB | 5.17:1 against a white page |
| Color, dark theme | Blue 400 #60A5FA | About 7:1 against #18181B |
| Applies to | All interactive elements, via :focus-visible | Shows for keyboard use, avoids rings on mouse clicks |
Watch for the trap where the focus color matches the button. A Blue 600 ring drawn flush against a Blue 600 button has no contrast with the adjacent pixels at all; the 2px offset fixes that by putting white page between them. Also tab through the page with a sticky header or cookie banner showing: a focused link that scrolls completely underneath the header fails 2.4.11, however good its ring looks. Setting CSS scroll-padding-top to the header height is one way to handle it.
The WCAG ratio is not the only way to rate a dark pair, and the two methods disagree most on mid-grey text over near-black. Our APCA versus WCAG contrast guide shows the numbers.
Dark theme is a second set of pairs
A dark theme is not the light palette inverted. It is a new set of pairs, measured again, because luminance does not flip symmetrically. Here is what happens to the light-theme tokens when the page background becomes #18181B:
| Token | Hex | Ratio on #18181B | Verdict in dark theme |
|---|---|---|---|
| Primary text | #F5F5F4 | 16.24:1 | Body text |
| Muted text (light theme's neutral-500) | #71717A | 3.67:1 | Fails for body text |
| Muted text (neutral-400) | #A1A1AA | 6.91:1 | Muted text, captions |
| Link (Blue 500) | #3B82F6 | 4.82:1 | Passes, barely |
| Link (Blue 400) | #60A5FA | 6.97:1 | Preferred link color |
| Danger text | #F87171 | 6.41:1 | Error messages |
| Success text | #4ADE80 | 10.17:1 | Status text |
| Raised surface | #27272A | 1.19:1 | Needs a border to read as a separate layer |
Three adjustments come out of that table. Muted text moves one step lighter, not darker. Semantic colors move to their lighter steps (400 or 300 instead of 600 or 700). And raised surfaces such as cards and menus barely separate from the page by luminance alone, so they need a visible border or a larger lightness gap when the surface boundary matters. The dark mode design guide covers surface elevation, image handling and theme switching in more depth.
Brightness also changes perception. A fully saturated blue at the same contrast ratio can look much more intense on dark gray than on white, so many teams reduce chroma for large dark-theme fills and reserve the most saturated values for small interactive elements. That is a visual judgment rather than a WCAG rule, so check it on real screens.
Once the text and component pairs pass, the next check is whether your categorical colours stay apart for colour-blind users; color-blind friendly palettes tests the Okabe-Ito and Paul Tol palettes and shows how to run the same test on yours.
Text on images and gradients
Hero banners and social graphics put text on top of photography, and the 1.4.3 requirement still applies to that text. The exemption for "text in a picture that contains significant other visual content" covers incidental text inside a photo, such as a shop sign, not the headline you place over it.
A workable process for overlay text:
- Find the lightest region the text will cross at every breakpoint, because cropping changes which part of the image sits behind the headline.
- Sample that region's color and test it against the text color.
- If it fails, add a scrim: a semi-transparent dark layer or gradient between the image and the text. Start with a moderate opacity and raise it until the lightest sampled point passes, rather than picking a fixed percentage up front. For frosted panels that use backdrop-filter, the glassmorphism text contrast guide works out the minimum opacity for white and dark text.
- For gradient buttons and banners, test the text against both ends of the gradient and the midpoint. The weakest of the three is the real ratio.
- Recheck when the image changes. Marketing images are swapped far more often than components, and the scrim tuned for one photo can be too light for the next.
For exported graphics that go to print, contrast checks still use sRGB values. Converting the file to CMYK can shift colors enough to change readability, so it helps to proof the printed piece too. The CMYK vs RGB print color guide explains why screen and press values differ.
A worked audit: one card, four fixes
Here is a typical pricing card as first designed, then corrected. The card sits on a white page.
| Element | As designed | Measured | Problem | Fix | After |
|---|---|---|---|---|---|
| Card fill | #F4F4F5 | n/a | None | None | n/a |
| Plan caption | #71717A on card | 4.40:1 | Fails 1.4.3 by 0.1 | Use #52525B | 7.03:1 |
| Price, 32px bold | Blue 500 #3B82F6 on card | 3.35:1 | Passes as large text only | Keep, it is large | Large-text pass |
| Feature checkmarks | Green 600 #16A34A on card | 2.998:1 | Fails 1.4.11 by a hair; on white it would pass at 3.30:1 | Use Green 700 #15803D and add a cross icon for excluded features | Above 4.5:1 |
| CTA button | White on Green 600 | 3.30:1 | 16px label fails 1.4.3 | Use Green 700 #15803D | 5.02:1 |
| Focus ring | Green 600 flush to button | about 1:1 against the button | Ring invisible against the fill | 2px Blue 600 ring, 2px offset | 5.17:1 against page |
None of these fixes changed the look of the card in a way a stakeholder would notice. The caption got slightly darker, the button one step deeper, the ring a different hue. Fixes at design time are usually this small; the same issues found after launch need code changes across every page that used the pattern.
Common mistakes and where the simple rule breaks
- Testing swatches instead of placements. The neutral-500 caption example above passes on one surface and fails on another. Test the pair where it is used.
- Rounding a near miss. 4.48:1 is a fail. Some tools display one decimal place; read the full value.
- Treating semibold as bold. The 14pt bold large-text rule refers to bold weight. If your font's semibold looks lighter than a typical bold, treat it as regular text and use the 4.5:1 threshold.
- Using thin fonts at the minimum ratio. WCAG measures color, not stroke width. A 300-weight font at exactly 4.5:1 reads worse than a 400-weight font at the same ratio, so leave headroom for light weights.
- Forgetting translucent colors. A text color with 70% opacity takes on the background's color. Measure the blended result on each background it will appear on, not the token's hex value.
- Assuming AAA is required. 1.4.6 is Level AAA, and W3C's conformance guidance says requiring Level AAA as a general policy for entire sites is not recommended, because some content cannot satisfy every AAA criterion. Use 7:1 as a target for long-form reading surfaces where it is achievable, not as a blocker for every component.
- Assuming brand colors are exempt. Only the logo is. Brand blue used as link text must pass like any other text color.
The handoff document
A color system lives in its documentation. For each theme, the handoff should contain:
- The token list with hex values and the role each token may fill.
- The approved-pairs matrix, with measured ratios and the size each pair is approved for.
- A short do-not-use list naming the pairs that look tempting and fail (white on green 600, neutral-500 on the card fill).
- Component state tables for buttons, inputs and toggles.
- The focus indicator spec.
- The chart palette in order, with its dark-theme equivalents.
- Two or three annotated screenshots showing the tokens in place.
Keep this inside the design system's documentation rather than in a separate file that drifts. The guide to building a design system from scratch covers token naming and structure, and the brand style guide walkthrough shows where color rules sit alongside logo and type rules for external partners.
FAQ
Does every color in a brand palette need to pass contrast?
No. A palette can include colors that never touch text or controls, such as illustration fills or decorative backgrounds. The requirement attaches to placements: text, the visual parts of controls and their states, focus indicators, and graphics needed to understand content. Mark decorative-only colors as such in the token list so nobody uses them for text later.
Is 4.5:1 enough for long articles?
It is the AA floor, and AA is the level most accessibility policies and audits reference. For long reading surfaces such as article bodies and documentation, aiming for the AAA value of 7:1 gives readers with low vision more margin, and it is usually easy to reach for black-on-white body text. Smaller UI text such as captions is where 7:1 becomes hard, and AA remains acceptable there.
Do icons need 3:1 or 4.5:1?
Icons are non-text, so 1.4.11 sets 3:1 when the icon is needed to understand the content or to identify a control. An icon that sits beside a text label that already conveys the same meaning is supplementary, and the 1.4.11 guidance treats graphics whose information is also in visible text as not requiring the threshold. Icon-only buttons have no such backup, so give them 3:1 at minimum.
Why does my contrast checker disagree with my design tool?
Usually because they are measuring different colors. Design tools may sample a blended value when layers use opacity, blend modes or shadows, while a checker tests the raw hex values you typed. Another source is color profile handling: a file working in Display P3 or a CMYK profile can show different numbers than the sRGB values the WCAG formula assumes. Test the final sRGB values the browser will render.
Can OKLCH lightness replace contrast checks?
No. OKLCH lightness approximates perceived lightness, but WCAG contrast is calculated from relative luminance, a different measure. Two colors with the same OKLCH lightness can sit on different sides of 4.5:1 against white. OKLCH is a good tool for building even ramps; the contrast formula still decides what passes.
Sources consulted
- Text contrast thresholds, large-text sizes, the no-rounding rule and the luminance formula: W3C's explainer for 1.4.3, https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html (checked October 22, 2026)
- The 7:1 enhanced level for body copy: W3C's explainer for 1.4.6, https://www.w3.org/WAI/WCAG22/Understanding/contrast-enhanced.html (checked October 22, 2026)
- Control boundaries, states, adjacent colors and focus contrast: W3C's explainer for 1.4.11, https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html (checked October 22, 2026)
- Color as the only cue: W3C's explainer for 1.4.1, https://www.w3.org/WAI/WCAG22/Understanding/use-of-color.html (checked October 22, 2026)
- Keyboard focus must be visible at Level AA: W3C's explainer for 2.4.7, https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html (checked October 22, 2026)
- Focused items that end up entirely hidden behind sticky content: W3C's explainer for 2.4.11, https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum.html (checked October 22, 2026)
- The 2 CSS pixel perimeter test for focus rings: W3C's explainer for 2.4.13, https://www.w3.org/WAI/WCAG22/Understanding/focus-appearance.html (checked October 22, 2026)
- Why AAA is not a site-wide requirement: W3C's conformance explainer, https://www.w3.org/WAI/WCAG22/Understanding/conformance (checked October 22, 2026)
- Links set apart by color alone: W3C technique G183, https://www.w3.org/WAI/WCAG22/Techniques/general/G183 (checked October 22, 2026)
- Which focus criteria arrived in version 2.2, and the release date: W3C WAI summary of changes, https://www.w3.org/WAI/standards-guidelines/wcag/new-in-22/ (checked October 22, 2026)
Contrast ratios in the tables were calculated by the Lucky Graphics editorial team with the relative luminance formula from the 1.4.3 Understanding document.
Next step
Put your own palette through the same test: open the WCAG 2.2 accessibility compliance guide to see how color fits with keyboard, structure and form requirements before an audit, so the color fixes you make here land alongside the rest of the checklist.