notes on color systems

In many, if not all, of the projects I work on, CSS colors tend to devolve into a mess of hard coded color values. Palettes have improved the situation, mostly by replacing those hardcoded values with a smaller set of green[400] or red[200] values. It still inherently lacks semantics and hints about say where a grey[800] vs a grey[900] might be preferable, or which the standard is for a border.

The probem I am solving for is making tweaks to site colors. I want to minimize the number of updates when say I change the primary accent from teal to orange.

looking for inspiration

Material design has some good ideas, but they also seem kind of complicated. Material 2 has main/dark/light/constrastText version, and primary/secondary colors. Material 3 is slightly better with its role-based color system, but there are so, so many roles.

https://grayscale.design talks about using a "color value-first" approach to design and it resonated, but ultimately still resorts to using a palette generator.

Lately there's been a flurry of posts on the CSS oklch color space as an alternative to rgb (hex) or hsl colors - I think starting with this Evil Martians post and then this more accessibly-written piece which sparked a long discussion on hacker news.

oklch and relative colors

I like the idea of building on oklch to create color-invariant themes where elements have a constant relative perceived brightness. By picking a single base color I can generate related colors automatically using relative css colors.

One hitch is that there's no way to also automatically select a good contrasting color for text. There’s an early CSS spec for contrast-color - "a sufficiently contrasting color against a specified background or foreground color without requiring manual computation", but it’s going to be a while before that shows up in browsers, and moreover it currently only supports picking white or black. So we’re also going to need to explicitly specify the text color in addition to the base color.

The other hitch is that css variables that reference other variables resolve those values when they are defined, not when they are used. For example --light-variation: oklch(from var(--base) calc(l + 0.1) calc(c * 0.75) h) would capture the original --base, but if --base is subsequently changed that would not be reflected in the value of var(--light-variation).

Okay, one more thing - oklch has only existed since 2023, and relative colors from 2024, so if you need to support older browsers you’ll need to something like postcss to transform it for compatibility.

where I’m at

What I’ve settled on for now is generate a set of palettes driven by single colors, i.e. something like the following. This does an okay job of separating the choice of color from the brightness of the specifc color I need for a style element like the border.

Note that the color calculations don't always generate valid oklch colors, but browsers will pick something close enough so it sort of works.

.teal {
--base: oklch(0.552 0.095 180.5);
--text: white;
--light1: oklch(from var(--base) calc(l + 0.1) calc(c * 0.75) h);
--light2: oklch(from var(--base) calc(l + 0.2) calc(c * 0.75) h);
--light3: oklch(from var(--base) calc(l + 0.3) calc(c * 0.75) h);
--dark1: oklch(from var(--base) calc(l - 0.1) calc(c * 0.75) h);
--dark2: oklch(from var(--base) calc(l - 0.2) calc(c * 0.75) h);
--dark3: oklch(from var(--base) calc(l - 0.3) calc(c * 0.75) h);
}

input[type=text] {
border: 1px solid var(--light-2);
}

/*
<form class="teal">
<input type="text" />
</form>
*/