Always Twisted

Quick and Dirty Colour Palettes using color-mix()

On This Page:

When I first started tinkering on the idea of a redesign of this site in ~2019 (yeah, it took that long) I started prototyping the layout (which changed) and other aspects of the site (theming) using CodePen.

To make things easier with my idea for theming (having lighter and darker variations of the primary colour for the background, foreground, etc.) I looked at using the (then) relatively new CSS feature color-mix().

What is color-mix()?

Simply put, rhe color-mix() CSS function lets you blend two colours together. You also have the option to choose how much of each colour you want to mix and pick the colour space it works in, like srgb, or display-p3.

This makes it great for creating simple colour palettes with:

The syntax for color-mix is quite simple:

Code languagecss
color-mix(in <color-space>, <color-1> <percentage-1>, <color-2> <percentage-2>)

Here's what each part means:

  1. <color-space> specifies the colours space to do the blending in, common options are: srgb and display-p3.
  2. <color-1> is the first colour in the mix, this could be a custom property. a named colour, or a colour code.
  3. <percentage-1> denotes the proportion of the first colour to be blended.
  4. <color-2> is the second colour that is to be blended.
  5. <percentage-2> would be how much of the second colour is mixed with the first.

You can also omit one of the percentages, the remaining percentage is automatically calculated:

Code languagecss
color: color-mix(in srgb, #BADA55 70%, #C0FFEE 30%);

Can be shortened to either:

Code languagecss
color: color-mix(in srgb, #BADA55 70%, #C0FFEE);

Or:

Code languagecss
color: color-mix(in srgb, #BADA55, #C0FFEE 30%);

Adam explains how color-mix() works and more in this Chrome Developers blogpost.

Creating Simple Colour Palettes

Let's start with a primary colour and black and white as CSS custom properties and generate some tints and shades using color-mix().

Code languagecss
:root {
--color-black: #000000;
--color-white: #FFFFFF;
--color-primary: #BADA55;
/* tints */
--color-primary--tint-10: color-mix(
in srgb,
var(--color-primary),
var(--color-white) 10%
);
--color-primary--tint-20: color-mix(
in srgb,
var(--color-primary),
var(--color-white) 20%
);
/* Shades */
--color-primary--shade-10: color-mix(
in srgb,
var(--color-primary),
var(--color-black) 10%
);
--color-primary--shade-20: color-mix(
in srgb,
var(--color-primary),
var(--color-black) 20%
);
}

With just a few lines, you’ve got lighter and darker variations that you can reuse for things like hover states, text, or borders.

Adding Opacity Variants

If you need semi-transparent colours, color-mix() works just as well. By blending with transparent, you can create a range of opacity levels:

Code languagecss
:root {
--color-primary: #BADA55;
/* Opaques */
--color-primary--opacity-10: color-mix(
in srgb,
var(--color-primary),
transparent 10%
);
--color-primary--opacity-20: color-mix(
in srgb,
var(--color-primary),
transparent 20%
);
}

These could be super handy for things like overlays, highlights, or subtle backgrounds.

Building A Colourful Neutral Palette

Your neutral colours, going from white to black could look a little better if they had some of your primary colour in the mix. Using color-mix() makes this super easy:

Code languagecss
:root {
--color-black: #000000;
--color-white: #FFFFFF;
--color-primary: #BADA55;
--color-primary--neutral-10: color-mix(
in srgb,
var(--color-black) 10%,
var(--color-primary) 3%
);
--color-primary--neutral-20: color-mix(
in srgb,
var(--color-black) 20%,
var(--color-primary) 3%
);
}

This can give your neutral colour palette a softer tone that feels connected to the primary colour.

If you define a percentage for both colours, but that percentage does not add up to 100% the remaining percentage of the colour is transparent.

Bringing It Together

So far, we've looked at creating simple tint, shade, opaque, and neutral colour palettes using the color-mix() function. Below is a demo of these colour palettes, include options to:

Creating a 'Dirty Hack' Dynamic color-mix()

For even quicker prototyping you can create a 'dynamic' custom property that uses the * selector.

Code languagecss
:root {
  --color-primary: #BADA55;
  --color-white: #FFFFFF;
  --mix-value: 50%;
}
* {
--color: color-mix(
in srgb,
var(--custom-color, var(--color-primary)),
var(--custom-mix-color, var(--color-white)) var(--mix-value)
);
}

What’s Happening Here?

  1. --custom-color: Allows you to set a custom colour, which falls back to the --primary-color;
  2. --mix-color: Sets a new colour to mix which defaults to --color-white;
  3. --mix-value: Controls the percentage of the mix dynamically.
  4. *: Declares the calculated --color and makes it available to all elements

Performance Issues with the * Selector

While the * selector is useful for experimentation, it’s not suitable for production due to potential performance concerns:

The Future with CSS Functions

Looking ahead, Functions and Mixins in CSS aim to replace common preprocessing patterns by giving CSS authors the ability to define reusable logic natively.

These functions operate in the value space of CSS, meaning they compute and return values like colours, lengths, or strings, which can then be assigned to properties.

Combining the CSS color-mix() feature with the proposed custom function syntax could create a better way for dynamic

Code languagecss
@function --tint-shade(--color: type(color), --mix-color: type(color), --percent: type(percentage)) returns type(color) {
result: color-mix(in srgb, var(--color), var(--mix-color) var(--percent));
}
.component {
background: --tint-shade(var(--primary-color), white, 30%);
}
.component:hover {
background: --tint-shade(var(--primary-color), black, 20%);
}

This CSS function takes two colours and a percentage and then returns the computed mixed colour for use anywhere it is called in the CSS.

Benefits and Concerns with Native CSS Functions and Mixins

Native CSS functions and mixins bring some exciting possibilities. They let you skip preprocessors like Sass or PostCSS, which can simplify your workflow by reducing dependencies and build steps.

That said, they’re not without challenges. Performance might take a hit with complex functions that need constant recalculations, like during animations. Planning reusable functions and mixins also requires thought, especially when juggling things like fallbacks or cascading rules.

The W3C is currently exploring this new specification to enhance CSS’s flexibility. As this specification is still under discussion, its details may evolve, it may be some time before we can experiment and use it in production. It's exciting, none-the-less.

Using Sass to Automate CSS Custom Properties

Whilst we're waiting for the cool new CSS with functions and mixins, and if you're already using Sass in your code base we can make things a little easier by using Sass mixins to generate the custom properties - nobody's got time to write (almost) the same code out 10, 20, 30, 40 times.

Here's a little Sass mixin that will generate the CSS custom properties for tint, shade, opaque, and neutral colour palettes

We will use a @for loop to help us, in this example we're only creating a tint colour palette of CSS custom properties:

Code languagescss
@mixin generate-color-variables($color-name, $base-color, $steps: 9) {
:root {
// Generate tints
@for $i from 1 through $steps {
--#{$color-name}--tint-#{$i * 10}: color-mix(
in srgb,
var(--#{$base-color}),
var(--color-white) #{$i * 10}%
);
}
}

To make use of the Sass mixin in our code we would need to @include it also:

Code languagescss
@include generate-color-variables("color-primary", "color-primary", 10);

This would generate a set of CSS custom properties:

Code languagecss
:root {
--color-primary--tint-10: color-mix(
in srgb,
var(--color-primary),
var(--color-white) 10%
);
--color-primary--tint-20: color-mix(
in srgb,
var(--color-primary),
var(--color-white) 20%
);
--color-primary--tint-30: color-mix(
in srgb,
var(--color-primary),
var(--color-white) 30%
);
--color-primary--tint-40: color-mix(
in srgb,
var(--color-primary),
var(--color-white) 40%
);
--color-primary--tint-50: color-mix(
in srgb,
var(--color-primary),
var(--color-white) 50%
);
--color-primary--tint-60: color-mix(
in srgb,
var(--color-primary),
var(--color-white) 60%
);
--color-primary--tint-70: color-mix(
in srgb,
var(--color-primary),
var(--color-white) 70%
);
--color-primary--tint-80: color-mix(
in srgb,
var(--color-primary),
var(--color-white) 80%
);
--color-primary--tint-90: color-mix(
in srgb,
var(--color-primary),
var(--color-white) 90%
);
--color-primary--tint-100: color-mix(
in srgb,
var(--color-primary),
var(--color-white) 100%
);
}

The Sass Mixin in Full

Here is the complete Sass mixin that would generate the four colour palettes as CSS custom properties using color-mix():

Code languagescss
// Mixin to generate CSS custom properties
@mixin generate-color-variables($color-name, $base-color, $steps: 9) {
:root {
// Generate tints
@for $i from 1 through $steps {
--#{$color-name}--tint-#{$i * 10}: color-mix(
in srgb,
var(--#{$base-color}),
var(--color-white) #{$i * 10}%
);
}
// Generate shades
@for $i from 1 through $steps {
--#{$color-name}--shade-#{$i * 10}: color-mix(
in srgb,
var(--#{$base-color}),
var(--color-black) #{$i * 10}%
);
}
// Generate opacities
@for $i from 1 through $steps {
--#{$color-name}--transparency-#{$i * 10}: color-mix(
in srgb,
var(--#{$base-color}),
transparent #{$i * 10}%
);
}
// Generate Neutral Colours
@for $i from 1 through $steps {
--color--neutral-#{$i * 10}: color-mix(
in srgb,
var(--color-black) #{$i * 10}%,
var(--color-primary) var(--neutral-percentage, 3%)
);
}
}
}

I hope I have showed you how simple it can be to quickly create your own colour palettes with color-mix(). Whether you're prototyping or advancing the colour system within your Design System, I think this is a great CSS feature to add to your CSS toolkit.

Are your CSS naming conventions consistent across your Design System?

I’ll create clear naming conventions that ensure consistency in your styles.

get in touch!