Darek Kay's picture Darek Kay

Designing an accessible color palette with magic numbers

A low text contrast is the most common accessibility issue. 86% of the top 1,000,000 websites have at least one contrast ratio violation, which may lead to a bad user experience. Our favorite orange website isn't leading by example, either. Some comments are almost unreadable:

Insufficient contrast ratio on Hacker News

There are various tools that help us identify and fix contrast ratio issues on our websites. This post presents an approach for designing and structuring color palettes so that we can prevent such issues before they arise: "magic numbers".

Which contrast ratio is accessible?

How do we know whether the contrast ratio between a text and its background is sufficient? The Web Content Accessibility Guidelines (WCAG) define minimum required contrast ratios, based on colors, text properties (size, boldness) and conformance levels (AA vs. AAA):

Level AALevel AAA
small text4.5+7+
large text3+4.5+
Minimum required contrast ratio values

Note: large text is defined as 19px+ bold or 24px+ normal.

As a rule of thumb, try to go for a 4.5:1 minimum contrast ratio, which will pass WCAG AA independent of the text size. This provides a good cost-value trade-off and is often the legal accessibility requirement.

Calculating the accessibility conformance manually is tedious. Instead, I suggest using a tool like contrast-ratio.com. DevTools in Firefox and Chrome provide great built-in browser support. Finally, axe-core will scan a website for all kinds of accessibility violations.

Those tools are a great way to find contrast ratio issues, but let me describe a technique to prevent them in the first place.

Magic numbers

Most color palettes divide their colors into grades (e.g. pink-10 ... pink-90).

Let's define a difference between two color grades as magic number, e.g.:

  • Colors: blue-80 and orange-30
  • Magic number: 80 - 30 = 50

What if we could find a magic number for the whole palette that ensures a sufficient color contrast between two colors? The first time I've heard about this concept was in a blog post from Phips Peter. I've learned about the U.S. Web Design System and was immediately hooked. The color system provides the following magic numbers:

  • A magic number of 40+ ensures a contrast ratio of 3+
  • A magic number of 50+ ensures a contrast ratio of 4.5+
  • A magic number of 70+ ensures a contrast ratio of 7+

By looking at the color names, I know that red-40 and gray-90 (= 50) will definitely pass WCAG AA (required contrast ratio 4.5+), while red-60 and gray-90 (= 30) will not. This leads to a fantastic designer/developer experience. I don't have to use a contrast checker or look anything up to ensure a sufficient contrast ratio. A difference of 50 or more is all I care about.

Calculating magic numbers for an existing color palette

Some libraries define their magic numbers in the documentation, but what about others?

I have written a11y-contrast to calculate the magic numbers for any color palette that follows a grade naming pattern (e.g. red-20). This tool also finds all violations for any given magic number. This way you can prevent regression issues after adding or adjusting a color value.

CLI output with magic numbers and a list of violations

I've calculated the magic numbers for some common color palettes:

IBM v1506070
IBM Carbon v2.1505070
Tailwind v1607080
Tailwind v28080-
Open Color---

Here are my takeaways:

  • USWDS defines by far the most colors (461!), and yet it uses the smallest magic numbers. This leads to a much wider spectrum of allowed color combinations than any of the other color palettes I've checked.
  • Open color doesn't have any magic numbers. This means I cannot reliably derive the contrast ratio from the color naming (e.g., gray-90/red-20 is fine, but red-90/red-20 is not).
  • Both USWDS and IBM Carbon previously contained minor violations, showing the importance of automatic tests.

Defining luminance bounds with fixed magic numbers

Predefined color palettes are great, but what if we want to change or add a color? What grade does a certain color map to?

Because the contrast ratio between two colors depends only on their luminance values, it is possible to create a mapping from a color to a grade between 0 and 100. For the USWDS color palette, here are the luminance bounds:

grademin luminance (%)max luminance (%)

Dan O. Williams — a USWDS maintainer — optimized those values for consistency instead of coverage. This means there are more colors that don't fit into any bound, even though they would technically pass the WCAG contrast ratio. While this setup is more constraining, I agree with the consistency benefits.

It's important that our color system be consistent and predictable, that users know what they're getting when they choose a color of a certain grade. This makes us inclined to favor smaller, more equal ranges, and consistent spacing between ranges.

If you want to calculate the luminance value and the potential USWDS grade of any color, check out my Color Tools.


Let me summarize all the theory into some actionable advice. There are two ways to create an accessible color palette with magic numbers:

  1. Create a color palette first and calculate the magic numbers with a11y-contrast. Depending on the colors, the magic numbers might be rather high or even non-existent (see Open Color).
  2. Define luminance bounds with fixed magic numbers and use only colors that can be mapped. This approach is more constraining and requires more work, but the result is a future-proof and consistent color palette.

If you don't want to go through all the work, I suggest you try out the USWDS color palette.

Related posts

Want to leave a comment?

Join the discussion at Twitter, Mastodon or Hacker News. Feel free to drop me an email. 💌

Designing an accessible color palette with magic numbers