Automatically creating an accessible color palette from any color? Sure!

How do you let your users chose any color, but still make sure that the colors make your app useful and accessible? You create rules.

Find even more details and resources here: https://confrere.com/a11y

To pick or not to pick a brand color

The natural way to let users configure their Confrere, so it looks more like their own brand, is to let them pick their own brand color. To have buttons, links and so on in the brand color seems like a no-brainer.

Should we really let them make this choice?

But it’s easier said than done. One brand might have a dark blue, another one a pale pink. Would you be able to read the text on a dark blue button? And would a pale pink button stand out as a call to action?

Letting users choose any color opened for a lot of issues, but it also made it clear that we wouldn’t be able to guarantee that the colors used in the interface had a sufficient contrast to be accessible (ast least 4.5:1 to fulfill WCAG AA). Not only am I convinced that striving for accessibility is the right thing to do — in Norway most of the WCAG AA criteria are enforced by law for any site or app directed at the general market.

So what do you do? Well, put a graphic designer (Eivind Molvær), a developer (Dag-Inge Aas), a designer/frontender (Jørgen Blindheim) and a UX designer (me) in a room and let them agree on some steps and rules.

Step 1: Get to know CSS Custom Properties

Central to our solution are CSS Custom Properties, more widely known as CSS Variables. These are values we can set and modify and reference at any point in our CSS, following the same cascading principles that we all know and love from CSS. With these, we can define any CSS value and reference it, which makes CSS Custom Properties perfect for storing color values.

body {
--color-primary: tomato;
background-color: var(--color-primary);
}

We are also able to set these properties from JavaScript easily, making it possible to create powerful theming engines that can change whole color schemes on the fly.

document.body.style.setProperty("--color-primary", "tomato");

With these tools in hand, we set out to create our solution.

Step 2: Black or white button text?

First off, we figured we could do a simple contrast check to see if the brand color validated against the black button text. If the brand color doesn’t validate against black, we change the button text to white. With a little help from the npm package color, we can do this automatically:

// in Theme.js
import Color from "color";
const primaryColor = Color(brandColor);
const primaryColorContrast = primaryColor.light()
? Color("black")
: Color("white");
document.body.style.setProperty("--color-primary", primaryColor.string());
document.body.style.setProperty("--color-primary-contrast", primaryColorContrast.string());
// in Button.css
.button {
background-color: var(--color-primary);
color: var(--color-primary-contrast);
}
Yellow button? No need to change the color of the button text here :)
Dark blue button? White is a lot more legible than black!

Step 3: Generate lighter and darker color variants

We can’t use the same brand color everywhere. However, having the users specify their own secondary colors would add too much complexity. That’s how we came up with the idea to generate lighter and darker versions of the brand color.

To create the lighter version, we mix 20% of the brand color with 80% white. To create the darker version, we mix 80% of the brand color with 20% black, and then we check to see if it validates against white. We keep adding 10% black until the new darker color validates against white.

import Color from "color";
function getValidatedColor({
colorToChange,
colorToValidate = Color("white"),
minimumContrastRatio = 5,
mixingColor,
mixingAmount,
tries = 0,
maxTries = 8,
}) {
const newColor = colorToChange.mix(mixingColor, mixingAmount);
if (
newColor.contrast(colorToValidate) < minimumContrastRatio &&
tries < maxTries
) {
return getValidatedColor({
colorToChange: newColor,
mixingColor,
mixingAmount: 0.1,
tries: ++tries,
});
}
return newColor;
}
const primaryColor = Color(brandColor);
const primaryColorLight = getValidatedColor({
colorToChange: primaryColor,
mixingColor: Color("white"),
colorToValidate: Color("black"),
mixingAmount: 0.5,
});
const primaryColorDark = getValidatedColor({
colorToChange: primaryColor,
mixingColor: Color("black"),
mixingAmount: 0.2,
});
// then set these as CSS Custom properties

This way, you end up with automatically generated palettes like these:

Automatically generated color palette. The lighter version, the brand color, and the darker version.

Step 4: Extra outline on buttons?

With a very light brand color, the button itself would be almost invisible on the white background. So we do a contrast check again: If the brand color doesn’t validate against white, we add an outline to the button in the darker brand color. To achieve this, we must use some good defaults for our CSS Custom Property, and change that when we detect a too light color.

// in global stylesheet
body {
--button-border: none;
}
// In Theme.js
import Color from "color";
const primaryColor = Color(brandColor);
if (primaryColor.contrast(Color("white")) < 5) {
document.body.style.setProperty(
"--button-border",
"2px solid var(--color-primary-dark)",
);
}
// in button.css
.button.primary {
background-color: var(--color-primary);
border: var(--button-border);
}

This way, if the primary color validates, the button border will be “none,” and if it doesn’t validate, JavaScript updates the Custom Property to set a border value on the button. This then gets updated in realtime for all buttons on the page.

Thanks to the darker outline, this button is visible even though the brand color is a pale pink.

Step 5: Secondary colors with hue rotation

Rotating the turquoise brand color 60 degrees gives us a nicely matching blue color so that the top message in this form stands out from the call to action button.

But having lighter and darker versions of the brand colors wasn’t enough.

When working on the design for elements like warnings, error messages and notifications, we realized we needed a color that stood out from the brand color. But where would we find that?

By rotating the brand color 60 degrees, we could generate colors that matched the brand palette, but were was also different enough that they would stand out:

const noticeColor = primaryColor.rotate(60);

Try it yourself!

Like to test it out? Try the demo of the palette-generator.

Why should you care?

Dag-Inge (CTO) & Eivind (designer)

Originally, I thought of this as a specific and limited problem. But thinking about it, this applies to any scenario where users can pick their theming colors (for instance in your Twitter profile).

It seems the thinking these days is that when we let users pick colors, the user is responsible for any contrast issues.

I don’t think that’s fair to neither the user picking the color nor the end users. A sentence like “but does the color validate against white?” is meaningless to most people.

Making sure our interface has an accessible color scheme is our job.

We would love to hear ideas for how to improve the system and any other thoughts in the comments below :)