Creating the Accessible Color Palette Generator
Origins and retrospective
Creating the Accessible Color Palette Generator was a challenging but ultimately rewarding endeavor. Here’s how it came to be, what I learned, and what may lie ahead.
History and motivation
This journey actually began with seeds planted in 2007 before they fully blossomed in late 2024. Here’s a rough timeline of events:
2008
I was the designer for a startup in the recruitment space that published customized job seeker-facing landing pages for employers. I noticed potential accessibility problems with brand colors submitted by employers.
2016
I was the designer for a startup in the advertising space that published customized customer-facing landing pages for businesses. Again, I noticed potential accessibility problems with brand colors submitted by businesses. It occurred to me that it’d make sense to programmatically adjust brand colors for compliance.
2021
I designed a design system for a large enterprise. When I noticed designers were needlessly distracting themselves with picking colors, I thought it would make sense for the design system to be prescriptive about colors.
As part of its product offerings, the enterprise also sold white labeling as a feature. Not only did the colors need to be prescriptive, but the palette needed to be systematized for repeatability and even automation.
There clearly was an opportunity here.
With the help of our resident accessibility expert Alain Gravelet and inspired by an amazing talk by Ivy Wang at Figma’s 2021 Schema event, I designed my first accessible color system, as documented here:
Writing about this experience introduced me to Kevin Muldoon, a design systems technologist who also turned me onto APCA and other color-related concerns. Holy cow there are some smart people out there. 😅
December 2024
As part of a quick refresh of my personal website, I looked into guidelines for implementing dark mode. I applied the recommendation for desaturated colors and baked that into my approach. Here’s an article documenting how I went about it:
The article was well-received. Another design systems specialist, Mathias B, pointed me to Radix UI’s accessible colors. At a glance it, looked like any other gallery of color ramps. But then I found its custom palette tool.
It blew me away. Not only did Radix UI demonstrate that accessible color palettes could be programmatically generated, but it also demonstrated the value a prescriptive approach to the application of colors. I felt vindicated and inspired.
I was compelled to add my own spin to Radix UI’s custom palette tool, based on the approach that I outlined in my article as well as my design system. And unlike Radix UI’s approach, the tool would also generate a dark mode palette based on the accent color for light mode.
Great success!
I spent the 2024–2025 holiday season working tirelessly on the Accessible Color Palette Generator and released it in early January. It too was received positively, although its reach is limited by my modest social media presence.
What’s different from my article?
My article describes putting together a color palette for my very simple personal website:
- It wasn’t complex enough enough to require cards to help group and organize content-dense UIs.
- It assumed everything on the canvas is content (or what the WCAG calls “text“).
- It did not have any UI element that required soft surfaces or borders.
The Accessible Color Palette Generator takes additional complexity into account. It generates a color for cards for both content and non-content, as well as soft non-content colors.
I also made a few nomenclature improvements for the sake of aligning with naming conventions. The word to describe very prominent colors is now “strong” instead of the less descriptive (and potentially confusing) “default”. And to describe negligibly prominent colors used for some surfaces and orders I’ve opted to go with a more conventional “soft” instead of the “faint”.
Lastly, The Accessible Color Palette Generator also uses a different color model. Instead of lightness of HSL, it uses luminance of HCL for improved accessibility.
How it works
At the heart of the generator is a JavaScript function I wrote called adjustLuminanceToContrast()
. In short, it takes a foreground color (fgColor
) and adjusts it to meet a target contrast ratio against a background color (bgColor
). Then it returns the adjusted foreground color.
To help illustrate how tricky this was, here’s a graph comparing color luminance and contrast for different foreground and background combinations:
On the y-axis is a contrast ratio, which ranges from 1 to 21. On the x-axis is a color’s luminance, which rages from 0 to 1.
The blue line represents the target contrast ratio of 4.5:1 (targetContrast
).
The red line compares a black fgColor
against a white bgColor
. At minimum luminance, its contrast ratio is 21:0 against white. As you increase the luminance of fgColor
, the contrast ratio decreases until it maxes out at a contrast ratio of 1:1 against white forming an L-curve.
The yellow line compares a white fgColor
against a black bgColor
. At maximum luminance, its contrast ratio is 21:0 against black. As you decrease the luminance of fgColor
, its contrast ratio decreases until it maxes out at a contrast ratio of 1:1 against black, forming a straight line.
The green line compares a gray fgColor
against an identical gray bgColor
. Both colors having identical luminance, their contrast ratio is 1:1. As you increase or decrease the luminance of fgColor
, the contrast ratio forms a U-curve.
Where the lines intersect with targetContrast
is what luminance fgColor
should be set. That means:
- The black
fgColor
’s luminance should be increased to ~0.18. - The white
fgColor
’s luminance should be decreased to ~0.18. - The gray
fgColor
’s luminance should be decreased to ~0.01.
Here’s how the function works in greater detail:
- Get the initial luminances: The luminance of both
fgColor
andbgColor
is calculated. - Determine direction: The function guesses whether it should increase or decrease
fgColor
’s luminance to reachtargetContrast
. It does this by by slightly increasing and decreasingfgColor
’s luminance and sees compares their respective contrast ratios againsttargetContrast
. The winning direction is the luminance adjustment that results in a contrast ratio that is closest totargetContrast
. - Iteratively adjust luminance: A loop increases or decreases the luminance of
fgColor
by small increments until the contrast offgColor
andbgColor
is as close as possible totargetContrast
. - Return the adjusted color: Once the contrast is close enough to the target, the function returns the new adjusted
fgColor
.
Learnings
Working with GPTs
Because I’m a mediocre programmer at best, of course I leveraged GPTs to assist me through this project. They were most helpful with refreshing my memory about jQuery, Sass, and flexbox, but they were surprsingly unhelpful with the adjustLuminanceToContrast()
function.
GPTs kept suggesting an approach that involved binary search that yielded inconsistent results.
I suspect at the heart of the issue was that GPTs’ binary search approach assumed a more linear correlation between luminance and contrast ratios. But I didn’t want to waste my holiday season getting to the bottom of this, so I opted to go with a more straightforward approach. It’s probably not the most elegant solution, but perfect is the enemy of good.
Small effort, big implications
When I embarked on this journey, I simply wanted to put together a working concept. However, it eventually became an opportunity to demonstrate what a single resourceful product designer could deliver in under two weeks.
If this level of progress can be achieved with limited time and resources, imagine the possibilities when enterprises invest strategically in building and maintain robust design systems.
The question isn’t if design systems are worth the investment — it’s what’s holding organizations back from fully embracing their potential. Or perhaps the real challenge lies in how some organizations stifle individual initiative and innovation.
What the future holds
As promising as this project is today, the journey isn’t over. To truly unlock its potential, the next steps will focus on iterative improvements informed by dogfooding and thoughtful feedback. However, I do anticipate a few areas of focus:
WCAG indicators: I think the app can benefit from providing clear visual indicators of WCAG compliance, such as checkmarks alongside the calculated contrast ratios. This would help to turn abstract compliance into something designers can feel confident about.
Exporting results: Because design tokens are where designers and developers meet, it’d make sense to be able to export results in formats like JSON. Not only will this allow for integration with tools like Figma, but it’ll also pave the way for technical implementation.
Mobile experience: Inspiration can strike at any moment. That is why enabling the app to function effectively in mobile context can ensure that users can explore and refine color palettes wherever they are.
For more, I have a loosely prioritized list in the project repo’s README. In the meantime, I’m enjoying a nice little break.