CSS Variables and Sass mixins are each potent on their own. With some creativity, we can make them work together towards more flexible and robust solutions.
On several occasions, the designer I've worked with had used the same colour but with varying opacity. Many of his components were using varying shades of different colours.
The typical and tedious implementation involves declaring all colour variants as separate variables. We would usually end up with something similar to this:
/* Color palette */
:root {
--color-primary: #16498a;
--color-primary-a90: rgba(22, 73, 138, 0.9);
--color-primary-a80: rgba(22, 73, 138, 0.8);
--color-primary-a70: rgba(22, 73, 138, 0.7);
--color-primary-a60: rgba(22, 73, 138, 0.6);
--color-primary-a50: rgba(22, 73, 138, 0.5);
--color-primary-a40: rgba(22, 73, 138, 0.4);
--color-primary-a30: rgba(22, 73, 138, 0.3);
--color-primary-a20: rgba(22, 73, 138, 0.2);
--color-primary-a10: rgba(22, 73, 138, 0.1);
--color-secondary: #12284c;
--color-secondary-a90: rgba(18, 40, 76, 0.9);
--color-tertiary: #27add5;
--color-black: #000;
--color-gray: #ececec;
--color-light-gray: #f9f9f9;
--color-danger: #d63939;
--color-success: #4fc0b0;
--color-white: #fff;
}
Note that I prefer to use CSS Variables instead of standard SASS variables for their dynamic nature. Additionally, they help me write clean, readable, and modular code without having to import sass colour maps every time I want to reference a variable.
Our typical style guide comes with around 9 different colours and their variants. Our previous approach had several apparent issues. It produced large CSS files and made any slight change to our primary or secondary colours a considerable pain.
So how can we solve these problems?
The optimal solution would allow us to:
- Maintain a single source of truth for my colour definitions. Which, in my case, means that I should only have 9 variables for colours.
- Use any opacity variant of any of the brand colours without adding complexity.
- Apply changes to any brand colour by editing just one line of code.
- Fully utilize the power of dynamic CSS Variables.
Sass’s rgba mixin
First, I tried using the sass built-in 'RGBA' mixin. It seemed like a pretty straightforward solution.
border-top: rgba(#16498a, .4); // works.
border-top: rgba(22, 73, 138, 0.4); // works.
border-top: rgba(var(--color-primary), 0.4); // does not work.
Sass's RGBA function accepts 4 comma-separated parameters. However, it accepts two parameters if we wish to use hex values. Under the hood, Sass uses RGB/HSL functions to convert the hex colour value into RGB or HSL. Here is, in CSS, what the three examples above compile to:
// border-top: rgba(#16498a, .4); compiles to:
border-top: rgba(22, 73, 138, 0.4);
//border-top: (22, 73, 138, 0.4); compiles to:
border-top: (22, 73, 138, 0.4);
//border-top: rgba(var(--color-primary), 0.4); compiles to:
border-top: rgba(var(--color-primary), 0.4);
The example which used a CSS variable failed. Using SASS's rgba function with CSS variables fails to be rendered correctly.
According to the official CSS spec, "the values of custom properties are substituted as is when replacing var() references in a property's value".
However, these values are only interpreted at execution time. Which means that when SASS was being compiled to CSS, var(--color-primary) was not interpreted as a colour value. Instead, the compiler saw a random string and SASS's rgba function failed to be correctly compiled.
Moreover, this string can be anything as long as it is grammatically correct.
// For example, this is valid.
--foo: if(x > 5) this.width = 10;
// This code is obviously useless as a CSS variable. But can be used by javascript at run time.
So at compilation time, var(-- color-primary) is not a colour value at all; and it naturally fails to compile. However, lucky for me, it fails gracefully into the native rgba function.
/* Sass will fail to compile this line of code. But it fails gracefully and outputs the exact same line of code.
Now, when the browser interprets this code, it will try to use the native rgba function.
*/
Border-top: rgba(var(--color-primary), 0.4);
The native rgba function
According to the spec, the native rgba function only accepts 4 comma-separated values as parameters, which means that we cannot use hex colour values. Maybe we could try to declare our variables as comma-separated RGB values from the get-go.
:root {
--color-primary: 22, 73, 138;
}
div {
border-top: 1px solid rgba(var(--color-primary), 0.4) ;
}
This new method worked! We can now use opacity variants of any colour very quickly.
However, with this method, two new problems arose:
- A value such as '22, 73, 138' is not very readable: this method would require us to convert all my colour values into this format.
- We can no longer edit these values in a colour picker to test them out. Neither my IDE nor chrome dev tools can recognize these values as colours.
This method is time-consuming and is not expressive at all. However, we are getting closer to a cleaner solution
The solution
We want to be able to use CSS variables and not have to declare 10 opacity variants for each colour. It seems I have to use comma-separated RGB values, but I also need my code to be expressive and easy to edit.
:root {
--color-primary: #16498a;
--color-primary-rgb: 22, 73, 138;
}
h1 {
color: var(--color-primary);
}
h2 {
color: rgba(var(--color-primary-rgb), 0.4);
}
By declaring two versions of the same colour, one HEX and one RGB, we compromised on simplicity to make our solution work This approach increases the number of variables I initially aimed for, but it's a pretty good compromise.
So, we’ve managed to use CSS variables to create different shades of our colours. However, there is still some room for improvement. With this solution have two issues:
We still need to convert all our colours from hex to RGB format manually.
We have to edit multiple variable whenever we need to make a change to any of our colours.
Here is a function which converts hex colour values to RGB. The function extracts the red, green, and blue levels from any colour and returns them in a comma-separated format.
@function hexToRGB($hex) {
@return red($hex), green($hex), blue($hex);
}
:root {
--color-primary: #16498a;
--color-primary-rgb: #{hexToRGB(#16498a)};
}
With this function, we will no longer need to do the colour conversions manually.
The solution is now very close to what we set out to achieve. We still want to create a single source of truth for each of my variables.
@function hexToRGB($hex) {
@return red($hex), green($hex), blue($hex);
}
$color-primary: #16498a;
:root {
--color-primary: #{$color-primary};
--color-primary--rgb: #{hexToRGB($color-primary)};
}
Sass functions, mixins, variables, and native CSS variables are all powerful features. However, none of them provides a complete solution on its own. They need to work together to create robust solutions.
I initially set out to solve the problem of using CSS variables, or custom properties, in sass functions and mixins. With some compromise and much testing, I was able to create a satisfactory solution, which I hope you find useful.
Bonus
To further understand this concept and see an example of how it can be applied, I created this sample project on Stackblitz.
https://stackblitz.com/edit/react-jyogzp?file=index.js
I built a heatmap to show how a project's repo is progressing. Every box represents a calendar day, and its colour shows whether the codebase grew or shrunk in size.
The darker a blue tile is, the more code was added to the repo on that day.
Similarly, the darker a red tile is, the more code was removed from the repo on that day.
Gray tiles represent days when no work was done.
Top comments (1)
That explains why, initially, my compilation wouldn't work whenever I used var() with .sass variables and mixing. Thank you for providing a solution.