Skip to main content

Navigating Accessible Colors Through Semantics

Accessible and semantically accurate color palette can be more complex than it seems. 16.8 million colors are not always enough...

`color-utils` cover image
Client -
Year 2023
Project type Side ProjectDesign
Logo of React React
Logo of Vite Vite
Logo of TypeScript TypeScript
Logo of Material UI Material UI

Choosing an accessible and semantically accurate color palette can be more complex than it seems.

Even if 16.8 million of RGB colors might seem a lot, designing a WCAG AA compliant color palette reduces by a lot, when the goal is to design nice stuff.

Turns out, only 6 colors might be available.

Through extensive research into color theory, perception, and accessibility guidelines, I've gained valuable insight into effective color selection.

This research led me to create my color palette generator, tailored for monochromatic design and powered by Google's Material 3 and its procedural color generator library.
Full-page screenshot of color-utils
The tool automatically provides WCAG/APCA values for optimal accessibility across a spectrum of supported color combinations.
Screenshot of the "Random color" functionality
A dynamic color generator showcasing random color previews for the *-100 and *-900 color stops, offering a glimpse into their representation within the final palette.

The process

The first step was to define a scale of neutral colors.

In principle, the contrast between background and foreground is easy to get. WCAG AA only prescribes a contrast ratio of 4.5:1 for normal text.

As any website incorporates some grain of interactivity, also the color for "disable state" should be kept in account.

WCAG guidelines specifies that:

The visual presentation of the following have a contrast ratio of at least 3:1 against adjacent color(s):

  • Visual information required to identify user interface components and states, except for inactive components or where the appearance of the component is determined by the user agent and not modified by the author;

This applies, for instance, to the disabled state vs. the enabled state.

A bit of math

  • Pure white (#FFF) + 4.5:1 contrast ratio => #737373

  • #737373 + 3:1 contrast ratio => #292929

We got our Minimum Viable Black.


Things start to become complicated when we start thinking that pleasant experiences require way more colors.

Pure black and pure white do not occur naturally; our perception of the world is limited to shades.

  1. Using pure colors can strain users' eyes. I am one of those who struggles to focus on text when contrast is too high.

  2. It is common to use both primary and secondary colors to distinguish areas on a webpage and highlight the relationship between information.

    This doesn't need to meet any WCAG requirement, as long as such correlation is also conveyed by other meanings (e.g. margin between boxes, borders, or icons).

  3. The user's screen may not display the full sRGB spectrum, or the color differences may be imperceptible.


We get a full new range of constraints:

  • Primary background shouldn't be #FFF

  • Primary foreground shouldn't be #000

  • Primary background and foreground should be as compressed as possible

  • Secondary background should have a minimum of 1.05:1 contrast with the primary color.

  • Disable state should have a 3:1 contrast with foreground, and 4.5:1 with secondary background.

Why secondary colors can't be semantic, only decorative

Even if we start with a color that has an ample contrast ratio according to WCAG 2.1 guidelines, a color that meets a 3:1 ratio with #000 will only achieve a 7:1 ratio with #FFF.

If we'd want to convey some meaning solely using colors we'd need to comply with the 3:1 contrast ratio.

Forwarding the white by 3:1 means that #949494 should be used as background.

However, that'd end up in a 2:27 contrast ratio against #5A5A5A.

Diagram illustrating the challenges associated with achieving WCAG AAA compliance.
Full WCAG AAA compliance can only be achieved by incorporating the complete range of grays into the design. Even so, only one color is available to pick.

Back to the future. The final palette.

There has been quite a heavy iteration over a couple of days.

As stated in the introduction, this research was originally an attempt to procedurally generate a color palette from any kind of color.

In an attempt to make the underlying color more apparent, blacks have been heavily compressed.

Here's the earlier diagram with the new color stops.

Diagram illustrating the permissible WCAG valid colors for the 'disabled' state, considering our specific constraints.
While it might appear like a substantial range, there are only 6 colors available for the 'disabled' state, given our specific constraints.

In this iteration, I also had to consider the appropriate color to use for the links. I decided to maintain the traditional differentiation between visited and not-visited links, which is commonly found in unstyled HTML and reminiscent of the early days of the internet.

That also introduced its challenges, since the whole range of the three channels (R, G, and B) is not available in the RGB space.

In the end, the link barely meets WCAG AA requirements on a dark background.

  • 4.50:1 vs Dark secondary background

  • 3:11 vs Dark Foreground / Text Color

Only 12 different color stops met WCAG requirements.

The decision has been made to select the option that provides the highest contrast with the text.

Key findings

  • I wanted to experiment with APCA, which is the proposed color specification for WCAG 3.

    I found out that it's already an incredible tool for describing colors.

    Unlike the algorithm used by WCAG 2, APCA can return a numerical value that explains why an excessive contrast can lead to sub-optimal experience, as stated earlier.

  • Unfortunately, striving for an APCA score of -100 while adhering to the WCAG 4.5:1 and 3:1 contrast rules is still quite challenging.

    I'm not entirely happy with how the text looks on a dark background.

    I used the findings of this research to design this website — yet, I had to make a difficult decision regarding the font choices, text size, and line spacing to find a balance that works with these colors.

  • Yellow can be a tricky color to style.

    When using the Material Design library from Google, the color is blended with the primary neutral tone, resulting in a color that is not always yellow.

    A manual 'yellow drift' control has been therefore introduced to facilitate fine-tuning.

  • Automated color blindness testing still requires manual correction.

    However, thanks to my tool, I have been able to reduce the amount of time spent on adjustments on Adobe Colors to just 5 minutes.