Building a color scheme

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.

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.

preview of the light theme end results

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;
}
screenshot of the light colors all together
Sandbox on CodePen

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:

  1. Users will generally be in the dark while using this theme, so test in the dark.
  2. Colors should desaturate as to not vibrate on the screen due to being over-intense.

preview of the end result of the dark theme

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.