Skip to main content

Building Themes with CSS4 Color Features

By Erik Jung

Published on August 8th, 2016

Topics

I’ve been experimenting lately with a new way to apply color values in my CSS. Instead of setting explicit values for each color, I calculate the entire palette from a small set of base colors. It’s not an applicable solution for every project, but it might be an interesting alternative method to consider.

There’s something about the way I’ve traditionally applied CSS color values that seems off. It feels a bit arbitrary and rigid compared to the way I write rules for other CSS properties. I derive font size values from a modular scale. I make sure margin and padding values fall in sync with a vertical rhythm. When it comes to color properties, however, my approach is often no approach at all. Colors are simply chosen and bound to various properties.

I became accustomed over the past few years to using the built-in color functions of preprocessors like Less and Sass. Many of these functions provide some means to calculate new colors on the fly. Until recently, my use of these calculated colors was often limited to things like hover or focus state variations.

a {
  color: blue;
}

a:hover {
  color: lighten(blue, 20%);
}
Code language: SCSS (scss)

This type of color manipulation has evolved into a commonplace preprocessor feature. Having moved away from preprocessor-specific CSS syntax, I’m glad to see something similar evolving as a W3C standard as well. Slated for inclusion in CSS4, color() aims to provide the same color transformations I had relied on preprocessors for.

The color-mod function takes an existing color, and applies zero or more “color adjusters” to it, which specify how to manipulate the color in some way.

I learned about this specification from a PostCSS plugin (now part of my regular CSS workflow). The color-mod function provides a syntax for modifying one or more attributes of an input color. The resulting output is a derivative color calculated by the adjusters you provide arguments for. To perform the same hover adjustment from above using color(), you’d write something like this:

a:hover {
  color: color(blue lightness(+ 10%));
}
Code language: CSS (css)

At first I wasn’t a huge fan of this, as it didn’t seem to provide anything I wasn’t already used to getting from Sass or Less. However, I began using it anyway because I wanted to familiarize myself with the standardized format that would someday be part of CSS4. It wasn’t until I discovered something specific about its syntax that I began favoring color().

color(
  blue            /* from blue */
  lightness(+ 1%)  /* to 1% lighter than blue */
  lightness(- 1%)  /* to blue again */
  hue(+ 60deg)     /* to magenta */
  hue(+ 60deg)     /* to red */
  hue(- 120deg)    /* to blue again */
);
Code language: CSS (css)

The difference that I find so appealing is that you can pipe multiple adjusters together within the same function call. Each adjustment is made against the previous one’s output. Being a fan of functional programming and automation, this sparked my interest in finding broader uses for color().

I was particularly interested in finding some way to improve my method for making interchangeable color themes. My traditional approach involved sets of pre-defined “brand” and ”theme” colors. This mapping of brand colors (named for their appearance) to theme colors (named for their purpose) often resembles this:

:root {
  --theme-bg: var(--color-white);
  --theme-text: var(--color-black);
  --theme-primary: var(--color-red);
  --theme-secondary: var(--color-blue);  
  --theme-tertiary: var(--color-purple);  
}
Code language: CSS (css)

I think this arrangement works well when the input colors are predictable and unlikely to change, and this is usually the case in my experience. But when a UI requires more theme variation, this map-based approach can be cumbersome to maintain.

For example, if I change the hue of one of my swatches, I then have to reassess how the resulting value coordinates with its neighboring colors. Seemingly small changes to a single color could adversely affect how it pairs with other members of my palette.

Another aspect of theming that bothers me is how to elegantly handle the need for inverted palettes. Colors intended for light backgrounds don’t always retain adequate contrast when put on a darker canvas.

I might depend on a lighter or darker color for emphasis and expect an increase in contrast with the surrounding background.

a {
  color: var(--theme-primary);
}

a:hover {
  color: var(--theme-primary-darker);
}
Code language: CSS (css)

While this expectation might be met with either a lighter or darker background, it likely won’t be met with both.

What if instead of defining a large set of color values, I could define a set of operations to calculate the color variations needed? With these calculations, one could in theory supply only one or two input colors and then compute the derivatives needed to complete a theme.

I did some experimentation with the color adjusters currently supported by postcss-color-function. What I found is that with only a few of these adjusters, I could create a simple framework for theme development that produced pleasing results.

While it’s intuitive to pick colors that appear to have enough contrast with each other, it’s hard to tell how they’re perceived by others. The contrast() adjuster exists to help with this problem. It works by computing a color that is contrasting enough with a base color to meet accessibility guidelines.

This adjuster is similar to functions found in Compass and Less. You supply a base color and a threshold percentage for the desired contrast ratio. You can use it to create foreground colors from backgrounds, or vice versa. The adjuster doesn’t care if the base color is light or dark, which makes it useful in the creation of “inverse” color schemes.

html {
  background: #333;
  color: color(#333 contrast(50%));
}
Code language: CSS (css)

The percentage argument might seem unintuitive — it did to me at first. It affects how much contrast is applied beyond the accessible minimum. A value of 100% will always be pure black or white depending on the base color.

I usually begin a theme with at least one predefined “brand” or “accent” color set aside for inclusion in my palette. This color is usually the first choice for elements that need emphasis, like buttons and links. Acting as my primary swatch, it’s the clear choice for when an element needs a splash of color to look generally important. What’s less clear is how to address elements that need to stand out in specific hues, regardless of what’s available. Sometimes by convention things need to communicate “success” with green, or “danger” with red. What if I have no constants set aside for those hues?

The quickest solution is to just fill in the missing hues with values that look reasonably “good”. But what if you decide to change your primary color later on? Will those manually chosen ancillary hues still complement it?

The hue() adjuster is helpful in this situation. It’s capable of a few different behaviors depending on the argument you provide. The behavior I’m most interested in occurs when you pass it a relative degree unit. This works like the Less spin() function, or the Sass adjust-hue() function. It rotates the color wheel, affecting only the hue of a color without mutating its other attributes. With this adjustment, you can calculate a full palette of hues matching the saturation and brightness of your base colors.

:root {
  --hue-base: #d54032;
  --hue1: color(var(--hue-base));
  --hue2: color(var(--hue-base) hue(+ 60deg));
  --hue3: color(var(--hue-base) hue(+ 120deg));
  --hue4: color(var(--hue-base) hue(+ 180deg));
  --hue5: color(var(--hue-base) hue(+ 240deg));
  --hue6: color(var(--hue-base) hue(+ 300deg));
}
Code language: CSS (css)

If each calculated value is distributed around the color wheel by an equal degree of change, you’ll have all the hue bases covered. Changing the base color will result in newly calculated hues. Somewhere in this array, those reds and greens will be waiting.

I began including “subdued” or “muted” color variations after seeing this concept years ago in Bootstrap. At first, these variations were simply shades of gray. Then they were lighter or darker derivatives of other colors, minus some saturation. Then I read this article and started thinking about the mixing approach to creating these tonal variations. I have little knowledge of painting or art history, but I find the concept agreeable. Instead of subduing colors by adding black or white, subdue them by making them recede into the lighter or darker colors of your palette.

The blend() adjuster exists for this purpose, enabling you to create in-betweens from two base colors. The percentage argument controls how much the second color is blended into the first. A practical application for this would be to create a muted text color by blending the default text color with its assumed background.

The above examples show how color adjusters work individually when applied directly to color values. By applying them to custom properties instead, you can create reusable instructions for building an entire palette. Custom properties allow the abstraction of these calculations, decoupling them from the base colors.

:root {
  /* Base colors (w/ defaults) */
  --theme-bg:     var(--theme-base1, #fff);
  --theme-accent: var(--theme-base2, blue);

  /* Computed colors */
  --theme-fg:     color(var(--theme-bg) contrast(50%));
  --theme-muted:  color(var(--theme-fg) blend(var(--theme-bg) 50%));
  --theme-alt1:   color(var(--theme-accent) hue(+ 60deg));
  --theme-alt2:   color(var(--theme-alt1) hue(+ 60deg));
  --theme-alt3:   color(var(--theme-alt2) hue(+ 60deg));
  --theme-alt4:   color(var(--theme-alt3) hue(+ 60deg));
  --theme-alt5:   color(var(--theme-alt4) hue(+ 60deg));
}
Code language: CSS (css)

With this logic encapsulated as an algorithm, you can easily reuse it with different inputs. This approach makes it trivial to create new themes for the same UI while only needing to supply unique base colors for each.

While it’s not always viable, I’ve found this technique to be a workable solution so far. I’m eager to see more exploration and experimentation with color() to test its potential for improving the way we deal with colors in CSS.

The easiest way to start using the CSS4 syntax shown in this article is with the cssnext plugin suite for PostCSS. It includes both the postcss-color-function and postcss-custom-properties plugins.

Here are some tools and references I’ve found to be very helpful in my experiments with color:

Comments