Generating Colour Variants in CSS with color-mix

🤔
This post deviates from my normal woodworking focus. It assumes that you're familiar with css in general and custom properties in particular.

When I build a design system for a website, I like to define a family of lighter and darker variants of each colour. In the past, I've done this by manually generating each variant in one of two ways:

  • Using a design or colour management app to define each variant, then setting the hex/rgb values as css custom properties. This was slow to set up in the first place and made it difficult to adapt to changes later on. If I wanted to change the hue for a colour, I had to redefine all the variants and update all the custom properties.
  • Defining the colours in code using hsl(). This made it easier to define variants by adjusting saturation and lightness without affecting the hue, and to change the hue later on without affecting saturation and lightness. The downside is that most of the colour values I receive from other people aren't defined in hsl to begin with. I still needed a colour picker to translate from other colour spaces.

Granted, you've been able to solve these issues for years by using sass, but I prefer standard css. This post will demonstrate how we can use the new color-mix function to generate colour variants in code without requiring other languages or tools. (At the time of writing, the Baseline status for color-mix() is "widely available," meaning that it has been implemented in all major browsers for at least 30 months and can generally be used without worrying about compatibility.)

The first step is to define a custom property for the accent colour.

:root {
  --color-accent: #6795cb;
}

Defining a css custom property for the accent colour.

I used this technique to generate the color variants for the Ghost theme on this site, using the --ghost-accent-color custom property instead of a fixed accent colour. This allows the accent colour to be configured in the Ghost theme settings.

:root {
  --color-accent: var(--ghost-accent-color);
}

Setting the accent colour to the --ghost-accent-color custom property that Ghost injects in the theme css.

Having defined the accent colour, we can define tinted variants of white and black. Adding a subtle tint to white and black can soften them and help them sit more comfortably with the accent colour.

I define custom properties for pure white and black that I incorporate into the color-mix() function and use anywhere else I want the pure variant. You might consider this to be an unnecessary step because pure white (#fff) and black (#000) are constant, but I prefer to have everything defined in one place.

Next, I use color-mix() to generate the tinted variants. This function requires a colour interpolation method consisting of the keyword in followed by a colour space. (I'm using the oklch colour space.) The second argument is the base colour – white or black, in this case. The third argument is the colour to be mixed into the base colour, and the mix percentage. The result is assigned to a new custom property.

Increasing the mix percentage will push the result towards the accent colour. Decreasing it will push it towards the base colour. The key here is subtlety – I normally use a 1% mix of the accent colour for white, and around 5-10% for black. The result should still pass for white or black if you look at it in isolation. If the mix is too strong, it will start to look like a highly diluted version of the accent colour.

:root {
  --color-accent: #6795cb;

  --color-white: #fff;

  --color-black: #000;
  
  --color-white-tinted: color-mix(
    in oklch, 
    var(--color-white), 
    var(--color-accent) 1%
  );
  
  --color-black-tinted: color-mix(
    in oklch, 
    var(--color-black), 
    var(--color-accent) 10%
  );
}

Defining tinted variants of white and black using `color-mix()`.

We can use the same technique to generate lighter and darker variants of the accent colour. I name these using levels from 050 to 950, e.g. --color-accent-350, similar to how font weight levels are specified. Level 500 is the pure accent colour. The levels below 500 increase the mix of white into the accent colour by 10% per level, making them progressively lighter. The levels above 500 increase the mix of black into the accent colour by 10% per level, making them progressively darker.

:root {
  --color-accent-050: color-mix(
    in oklch,
    var(--color-accent),
    var(--color-white) 90%
  );
  
  --color-accent-100: color-mix(
    in oklch,
    var(--color-accent),
    var(--color-white) 80%
  );
  
  --color-accent-150: color-mix(
    in oklch,
    var(--color-accent),
    var(--color-white) 70%
  );
  
  --color-accent-200: color-mix(
    in oklch,
    var(--color-accent),
    var(--color-white) 60%
  );
  
  --color-accent-250: color-mix(
    in oklch,
    var(--color-accent),
    var(--color-white) 50%
  );
  
  --color-accent-300: color-mix(
    in oklch,
    var(--color-accent),
    var(--color-white) 40%
  );
  
  --color-accent-350: color-mix(
    in oklch,
    var(--color-accent),
    var(--color-white) 30%
  );
  
  --color-accent-400: color-mix(
    in oklch,
    var(--color-accent),
    var(--color-white) 20%
  );
  
  --color-accent-450: color-mix(
    in oklch,
    var(--color-accent),
    var(--color-white) 10%
  );
  
  --color-accent-500: var(--color-accent);
  
  --color-accent-550: color-mix(
    in oklch,
    var(--color-accent),
    var(--color-black) 10%
  );
  
  --color-accent-600: color-mix(
    in oklch,
    var(--color-accent),
    var(--color-black) 20%
  );
  
  --color-accent-650: color-mix(
    in oklch,
    var(--color-accent),
    var(--color-black) 30%
  );
  
  --color-accent-700: color-mix(
    in oklch,
    var(--color-accent),
    var(--color-black) 40%
  );
  
  --color-accent-750: color-mix(
    in oklch,
    var(--color-accent),
    var(--color-black) 50%
  );
  
  --color-accent-800: color-mix(
    in oklch,
    var(--color-accent),
    var(--color-black) 60%
  );
  
  --color-accent-850: color-mix(
    in oklch,
    var(--color-accent),
    var(--color-black) 70%
  );
  
  --color-accent-900: color-mix(
    in oklch,
    var(--color-accent),
    var(--color-black) 80%
  );
  
  --color-accent-950: color-mix(
    in oklch,
    var(--color-accent),
    var(--color-black) 90%
  );
}

Defining variants of the accent colour using `color-mix()`.

If you're designing a larger system with many colours, you can repeat this for each one. Adjust the levels as necessary – perfect 10% increments may not be optimal for all colours.

I also use this technique to define variants of grey by mixing pure white into the tinted black, in 10% increments. Because we're mixing white into black, a higher mix percentage results in a lighter shade of grey.

:root {
  --color-gray-100: color-mix(
    in oklch,
    var(--color-black-tinted),
    var(--color-white) 90%
  );
  
  --color-gray-200: color-mix(
    in oklch,
    var(--color-black-tinted),
    var(--color-white) 80%
  );
  
  --color-gray-300: color-mix(
    in oklch,
    var(--color-black-tinted),
    var(--color-white) 70%
  );
  
  --color-gray-400: color-mix(
    in oklch,
    var(--color-black-tinted),
    var(--color-white) 60%
  );
  
  --color-gray-500: color-mix(
    in oklch,
    var(--color-black-tinted),
    var(--color-white) 50%
  );
  
  --color-gray-600: color-mix(
    in oklch,
    var(--color-black-tinted),
    var(--color-white) 40%
  );
  
  --color-gray-700: color-mix(
    in oklch,
    var(--color-black-tinted),
    var(--color-white) 30%
  );
  
  --color-gray-800: color-mix(
    in oklch,
    var(--color-black-tinted),
    var(--color-white) 20%
  );
  
  --color-gray-900: color-mix(
    in oklch,
    var(--color-black-tinted),
    var(--color-white) 10%
  );
}

Defining variants of grey using `color-mix()`.

Incorporate these variants into your design system however you like. For example, if you use --color-accent-500 (the pure accent colour) as the text colour for buttons, you might use a darker variant like --color-accent-600 for the hover and active states, and one of the greys for the disabled state.

button {
  color: var(--color-accent-500);
}

button:hover,
button:active {
  color: var(--color-accent-600);
}

button:disabled {
  color: var(--color-gray-500);
}

Using the colour variants to style buttons.

You might not use every one of these variants in your styles – I rarely do – but having them makes it easy to tweak the contrast between elements without altering the basic hue.

Thank you for reading!

If you found this post helpful, please consider supporting me by subscribing or sending a tip. It will help fund my work and writing.