A foundational overview of how to establish a dynamic and configurable color scheme
In this post I want to share thinking on ways to manage multiple color schemes in CSS. Try the demo.
If you prefer video, here's a YouTube version of this post:
Overview
We'll build an accessible color system with custom properties and calc(), to
make a webpage that's adaptive to user preferences while keeping the authoring
experience minimal. We start with a base brand color and build a system of
variants from it: 2 text colors, 4 surface colors and a matching shadow.
This guide begins with defining all of the colors for each color scheme up front. Not until the very end are they used to change the page.
The Brand
Often, a brand color has already been established and is delivered as
hex or
rgb. This GUI Challenge
has a base brand color of #0af. Firstly, for this color system, the hex value
needs converted to
hsl.
* {
--brand: #0af;
--brand: hsl(200 100% 50%);
}
In order to enable a concept of darkening or lightening the brand color, by say 20%, the 3 channels of the hsl color value need extracted into their own custom properties, like this:
* {
--brand-hue: 200;
--brand-saturation: 100%;
--brand-lightness: 50%;
}
CSS can do math on those color properties, for example calc(var(--brand-lightness) -
20%) to decrease the lightness value by 20%. This is foundational to building a
color scheme as CSS can keep all colors in the same hue family by adjusting the
hsl saturation and lightness amounts.
Light theme
Each color variant will be marked with its matching scheme, in this case, each
is appended with -light.

Brand
Starting with the brand color, it's rebuilt by wrapping --brand-hue, --brand-saturation
and --brand-lightness custom properties inside the hsl () function parenthesis,
without any calculations:
* {
--brand-light: hsl(var(--brand-hue) var(--brand-saturation) var(--brand-lightness));
}
Text colors
Next, the essentials of a color scheme need text colors. In a light theme, text should be very dark. Notice how the lightness of the following colors is low, well under 50%.
* {
--text1-light: hsl(var(--brand-hue) var(--brand-saturation) 10%);
--text2-light: hsl(var(--brand-hue) 30% 30%);
}
--text1-light, since it's very dark at 10% lightness, keeps the heavy 100%
saturation so the brand color can still peek through into the dark dark navy.
--text2-light, it's not quite as dark as the 1st color, which is good as it's
a secondary color, and it's also much less saturated.
Surface colors
Surface colors are the backgrounds, borders and other decorative surfaces that text sits upon or within. In a light theme, these are the light colors, as opposed to the text colors which were dark. To create light colors with hsl, we'll use higher percentage values in the third lightness value. We'll also lower the saturation, so the light greys don't look too tinted.
* {
--surface1-light: hsl(var(--brand-hue) 25% 90%);
--surface2-light: hsl(var(--brand-hue) 20% 99%);
--surface3-light: hsl(var(--brand-hue) 20% 92%);
--surface4-light: hsl(var(--brand-hue) 20% 85%);
}
4 surface colors were created since decorative colors tend to need more
variants, for interactive moments like :focus or :hover or to create the
appearance of paper layers. In these scenarios, it's nice to transition
--surface2-light on hover to --surface3-light, so a hover results in an
increase of contrast (99% lightness to 92% lightness; making it darker).
Shadows
Shadows within a color scheme are above and beyond, but add a lifelike nature to the effect and help it stand out from unrealistic black based shadows. To do this, the color of the shadow will use the hue custom property, be slightly saturated with the hue but still very dark. Essentially building a very dark slightly blue shadow.
* {
--surface-shadow-light: var(--brand-hue) 10% 20%;
--shadow-strength-light: .02;
}
--surface-shadow-light is not wrapped in an hsl function. This is because the
--shadow-strength value will be combined to create some opacity, and CSS needs
the pieces in order to perform calculations. Skip to the rad shadow
section to learn more.
Light colors all together
No need to hunt around to find how any of the light colors are made, they are all in one place in the CSS.
* {
--brand-light: hsl(var(--brand-hue) var(--brand-saturation) var(--brand-lightness));
--text1-light: hsl(var(--brand-hue) var(--brand-saturation) 10%);
--text2-light: hsl(var(--brand-hue) 30% 30%);
--surface1-light: hsl(var(--brand-hue) 25% 90%);
--surface2-light: hsl(var(--brand-hue) 20% 99%);
--surface3-light: hsl(var(--brand-hue) 20% 92%);
--surface4-light: hsl(var(--brand-hue) 20% 85%);
--surface-shadow-light: var(--brand-hue) 10% calc(var(--brand-lightness) / 5);
--shadow-strength-light: .02;
}
Dark theme
Most brands don't begin with a dark theme, it's a variant of their primary, usually lighter, theme. Users, on the other hand, often choose a dark theme for different contexts, like night time. These factors have led me to keeping two things in mind with dark themes:
- Users will generally be in the dark while using this theme, so test in the dark.
- Colors should desaturate as to not vibrate on the screen due to being over-intense.

Brand
The light theme used the 3 brand hsl color channels values without alteration, the dark theme does not. The saturation is cut in half and the lightness reduced a relative 50%.
* {
--brand-dark: hsl(
var(--brand-hue)
calc(var(--brand-saturation) / 2)
calc(var(--brand-lightness) / 1.5)
);
}
Text colors
In a dark theme, the text colors should be light. The following colors have high values for lightness, putting them closer to white.