Accessible Color Descriptions for Improved Color Pickers

By Devon Govett

Recently, we released a suite of color picker components in React Aria and React Spectrum. These components help users choose a color in various ways, including a 2D ColorArea, channel-based ColorSlider, circular ColorWheel, preset ColorSwatchPicker, and a hex value ColorField. You can compose these individual pieces together to create a full ColorPicker with whatever custom layout or configuration you need.

Initial accessibility experience#


Accessibility is at the core of all of our work on the React Spectrum team, and ColorPicker was no exception. However, these components presented a significant challenge: colors are inherently visual, so how should we make them accessible for users with visual impairments?

Our initial implementation followed the typical ARIA patterns such as slider to implement ColorArea, ColorSlider, and ColorWheel, and listbox to implement ColorSwatchPicker. This provided good support for mouse, touch, and keyboard input, but the screen reader experience left something to be desired. Out of the box, screen readers would only announce raw channel values like “Red: 182, Green: 96, Blue: 38”. I don’t know about you, but I can’t imagine what color that is just by hearing those numbers!

Improving screen reader announcements#


We set out to improve the screen reader experience using textual descriptions of the colors that a user selected. To do this, we compiled an extensive list of color names from sources such as Procato and the CSS named color keywords, and used the Delta E algorithm to match the user’s selected color to the closest color name. This resulted in much more intuitive screen reader announcements such as “Moderate Cornflower Blue” instead of numeric values like “Hue: 200 degrees, Saturation: 60%, Lightness: 62%”. This was a significant improvement!

However, despite the improvement, this approach presented several challenges. First, it required a huge number of strings for all of the color descriptions. We had almost 700 named colors, each of which needed to be translated into the 34 different languages we support, resulting in almost 24,000 strings measuring over 1 MB gzipped in bundle size.

Screenshot of bundle analysis showing over 1 MB of bundle size accounted for by color names in many languages

It was also a question whether translating all of these color names between languages would even be feasible. Languages and cultures describe colors in different ways, and translating esoteric names like “Light brilliant amaranth” and “Pale persian blue” between languages might not make sense to people around the world. This would likely require creating different color lists for each language, rather than translating a single list – a monumental task that wouldn't scale as we added new languages.

Finally, in terms of accessibility, some of the color names were difficult to understand, even for native English speakers. For example, I'm not sure I would know the difference between "Arctic Blue", "Cornflower Blue", "Cobalt Blue", or "Persian Blue" without looking at them. This would pose a challenge for users with limited or no vision.

Generating color descriptions#


Our final solution requires only 30 short strings per language to generate a description of any color. This includes 13 hues (pink, red, orange, brown, yellow, green, cyan, blue, purple, magenta, gray, white, and black), along with the halfway points between them (e.g. red orange, yellow green, and blue purple), and modifiers for lightness (very dark, dark, light, and very light), and chroma (grayish, pale, and vibrant). These strings are combined together to form a full color description.

In addition to reducing the number of strings we need, these descriptions are also simpler, more universally understood, and more easily translated between languages. For example, the description of is “light pale cyan blue”, and the description of is “dark vibrant purple magenta”.

Our algorithm for generating color descriptions works in the OKLCH color space, recently standardized by CSS. This color space offers the advantage of uniform lightness across all hues, unlike HSL, where hues like blue appear significantly darker than hues like green or yellow at the same lightness value. The difference between HSL and OKLCH is shown below.

HSL
OKLCH

In HSL, certain hues also appear to shift as the lightness changes — for example, blue tends to shift toward purple as it gets lighter. This would lead to perceptually inaccurate descriptions, where colors that appear purple might be described as blue. OKLCH resolves this issue by maintaining a consistent hue across all lightness levels.

HSL
OKLCH

These properties of OKLCH allow us to generate perceptually accurate descriptions for any color. Once a color is mapped to the OKLCH color space, we determine its hue name by dividing the color wheel into segments. Since the hue channel is measured in degrees from 0 to 360, it's simple to find the closest hue name using the angles of each segment.

Pink
Red
Orange
Yellow
Green
Cyan
Blue
Purple
Magenta

If a hue falls at least halfway between two segments, the names of both segments are combined. For example, a hue between red and orange would be described as “red orange”. These combinations are separate localized strings in order to account for languages that have specific terms for mid-hues, such as “Rotorange” in German.

There are also a few additional special cases. For instance, dark orange is referred to as “brown”, while darker yellows tend to appear more green and are described as “yellow green”.

Orange
Brown
Yellow
Yellow Green

The hue name is combined with lightness (very dark, dark, light, very light) and chroma (grayish, pale, and vibrant) descriptors based on ranges in the L and C channels of the OKLCH color space to create a complete color description.

Very Light
Light
Dark
Very Dark
Lightness
Grayish
Pale
Vibrant
Chroma

The order that the hue, chroma, and lightness descriptors are combined varies by language. For example in English we say "{lightness} {chroma} {hue}", but in Italian the order is "{hue} {chroma} {lightness}". This flexibility is achieved by using placeholders, allowing our translators to determine the appropriate arrangement for each language.

Check out the color picker below to see the results of this algorithm:

Final result#


After developing this algorithm to generate color descriptions, we integrated it into all of our color picker components. Since the same description may be generated for a range of colors, our components also announce the precise numeric value of the channels being modified. For example, a hue slider may announce “260 degrees, blue purple, slider”. Numeric values are useful for fine adjustments, while the color descriptions provide an overall sense of the color, similar to how one would perceive it visually.

The video below shows interacting with a ColorArea with color descriptions. You can also try it yourself with a screen reader in the example above.

Check out our ColorPicker components in React Aria to build accessible, customizable, and styleable color pickers in your own applications.