CodyHouse Framework + Components are featured on Product Hunt! Join the discussion →

How to combine SASS color functions and CSS Variables

A new method, supported in all browsers, to store your colors in CSS Variables and modify them using SASS functions.

CSS Variables are great. We all know that. HSL color values are the best. Agreed! SASS color functions are awesome. Yep, nothing new. But how to combine these things and use them TODAY? There's a way!

We've developed a new method for our framework that combines the flexibility of native variables (storing HSL color values) with the practicality of SASS functions.

👋 First time you hear about the CodyHouse Framework?

The problem #

In our framework, we use CSS Variables. We've integrated a modified version of the postcss-css-variables plugin to generate a fallback for browsers that don't support them. We preferred CSS Variables over SASS variables because you can overwrite their value at specific breakpoints (or using classes). This feature proved particularly useful to develop our responsive spacing and typography systems, and the color themes.

That said, here's how we defined the color variables when we launched the framework (v 1.0.0):

:root, [data-theme="default"] {
  // main
  --color-primary-darker: hsl(220, 90%, 36%);
  --color-primary-dark: hsl(220, 90%, 46%);
  --color-primary: hsl(220, 90%, 56%);
  --color-primary-light: hsl(220, 90%, 66%);
  --color-primary-lighter: hsl(220, 90%, 76%);
  --color-primary-a20: hsla(220, 90%, 56%, 0.2);

  --color-accent-darker: hsl(355, 90%, 41%);
  --color-accent-dark: hsl(355, 90%, 51%);
  --color-accent: hsl(355, 90%, 61%);
  --color-accent-light: hsl(355, 90%, 71%);
  --color-accent-lighter: hsl(355, 90%, 81%);

  // color contrast
  --color-bg: hsl(0, 0%, 100%);
  --color-bg-a00: hsla(0, 0%, 100%, 0);
  --color-contrast-lower: hsl(0, 0%, 95%);
  --color-contrast-low: hsl(240, 1%, 83%);
  --color-contrast-medium: hsl(240, 1%, 48%);
  --color-contrast-high: hsl(240, 4%, 20%);
  --color-contrast-higher: hsl(240, 8%, 12%);
  --color-contrast-higher-a90: hsla(240, 8%, 12%, 0.9);

  // semantic
  --color-border: var(--color-contrast-low);

  // ...
}

HSL color values are great because they make it intuitive to create color variations. Just edit the hue, saturation and lightness values. The numbers are easy to read.

However, when we started working on the components, we realized there was no easy way to set an alpha value for a color:

.component {
  background-color: hsla(var(--color-primary), 0.2); // not working 😥
}

You can include CSS variables into SASS mixins and functions, but the code above would return an invalid value (--var(color) is replaced by hsl(x,x%,x%)).

How to fix this?

Test #1 - Using CSS color-mod functions with a PostCSS plugin 🙅🏻‍♂️ #

First, we tried using CSS native color functions.

.component {
  background-color: color-mod(var(--color-primary) alpha(20%));
}

They're neat, but they're far away from being supported in major browsers. We tried integrating a few PostCSS plugins to generate a fallback, but with no luck. We kept getting errors with the final output, so we decided this method was too unreliable.

Test #2 - Setting CSS variables for the alpha values 🙅🏻‍♂️ #

Because our issue was not being able to set opacity values for the colors, approach number two was creating CSS variables for the alpha values:

:root, [data-theme="default"] {
  --color-primary: hsl(220, 90%, 56%);
  --color-primary-a20: hsla(220, 90%, 56%, 0.2);
}

On a component level, you would set an alpha value by applying an alpha variable:

.component {
  background-color: var(--color-primary-a20);
}

Bear in mind the goal was storing all the color values into a single _colors.scss file so that the whole system is easy to maintain. We couldn't just use hsla on a component level.

Although we weren't 100% sold on this solution, we decided to adopt it in the first version of the framework.

New solution - creating a SASS mixin to clean the colors mess 🎉 #

Creating variables for the alpha values proved to be a bad solution for two main reasons:

  1. If you're working on a component level, you have to switch to the _colors.scss file anytime you want to use a different alpha value for a color.
  2. Because our system is based on color themes (colors are interchangeable), if you create an alpha variable for a color, you have to do the same for all the other colors.

We were back to square one.

Here's the process that ended up with what we think is a great solution: first, we tried using a mixin to specify an alpha value. This mixin requires 3 variables: $property, $color-variable and $opacity.

We would use the mixin like that:

.component {
  @include alpha(background-color, --color-primary, 0.2);
}

While here's the code of the mixin:

@mixin alpha($property, $color-variable, $opacity) {
  $color-variable-h: var(#{$color-variable+'-h'});
  $color-variable-s: var(#{$color-variable+'-s'});
  $color-variable-l: var(#{$color-variable+'-l'});
  #{$property}: hsla($color-variable-h, $color-variable-s, $color-variable-l, $opacity);
}

For this to work, we needed to set 3 variables for each color:

:root, [data-theme="default"] {
  --color-primary: hsl(220, 90%, 56%);
  --color-primary-h: 220;
  --color-primary-s: 90%;
  --color-primary-l: 56%;
}

Where --color-name-h is the hue value, --color-name-s is saturation percentage and --color-name-l is lightness percentage (No, we didn't want to give up HSL color values ☝️).

Still too complicated, but we were getting somewhere.

At this point, we came up with the idea of creating our own alpha SASS function to replace the mixin. This would allow us to write a CSS declaration way more friendly:

.component {
  background-color: alpha(var(--color-primary), 0.2);
}

Here's the code of the function:

// return css color variable with different opacity value
@function alpha($color, $opacity){
  $color: str-replace($color, 'var(');
  $color: str-replace($color, ')');
  $color-h: var(#{$color+'-h'});
  $color-s: var(#{$color+'-s'});
  $color-l: var(#{$color+'-l'});
  @return hsla($color-h, $color-s, $color-l, $opacity);
}

// replace substring with another string
// credits: https://css-tricks.com/snippets/sass/str-replace-function/
@function str-replace($string, $search, $replace: '') {
  $index: str-index($string, $search);
  @if $index {
    @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
  }
  @return $string;
}

Getting closer! The last thing that bugged us was having to manually create 3 additional variables for each color (hue, saturation, lightness). It's annoying because you have to update the values of these variables anytime you modify a color.

The aha moment was realizing we could use a SASS mixin to define each color so that we could generate hue, saturation and lightness values automatically in CSS:

@mixin defineColorHSL($color, $hue, $saturation, $lightness){
  #{$color}: unquote("hsl(#{$hue}, #{$saturation}, #{$lightness})");#{$color}-h: #{$hue};#{$color}-s: #{$saturation};#{$color}-l: #{$lightness};
}

:root, [data-theme="default"] {
  @include defineColorHSL(--color-primary, 220, 89%, 56%);
  @include defineColorHSL(--color-accent, 355, 90%, 61%);
  @include defineColorHSL(--color-black, 240, 8%, 12%);
  @include defineColorHSL(--color-white, 0, 0%, 100%);
  // color contrasts
  @include defineColorHSL(--color-bg, 0, 0%, 100%);
  @include defineColorHSL(--color-contrast-lower, 0, 0%, 95%);
  @include defineColorHSL(--color-contrast-low, 240, 1%, 83%);
  @include defineColorHSL(--color-contrast-medium, 240, 1%, 48%);
  @include defineColorHSL(--color-contrast-high, 240, 4%, 20%);
  @include defineColorHSL(--color-contrast-higher, 240, 8%, 12%);
}

The good thing about this approach is that you still declare colors in a syntax that is easy to understand and that allows you to create color variations modifying HSL values:

:root, [data-theme="default"] {
  @include defineColorHSL(--color-primary-darker, 220, 90%, 36%);
  @include defineColorHSL(--color-primary-dark, 220, 90%, 46%);
  @include defineColorHSL(--color-primary, 220, 90%, 56%);
  @include defineColorHSL(--color-primary-light, 220, 90%, 66%);
  @include defineColorHSL(--color-primary-lighter, 220, 90%, 76%);
}

How to set alpha values on a component level #

In SCSS, you can set an opacity value using the alpha function:

.component {
  background-color: alpha(var(--color-primary), 0.2); // it works 🎉
}

It works 🙌! We just pushed a framework update (v 1.1.0) that includes the new mixins and an updated _colors.scss file.

Using this method to create additional color functions #

Because this method allows you to access and modify hue, saturation, lightness and alpha values, you can create a function for each one of them!

Edit lightness:

@function lightness($color, $lightnessMultiplier){
  $color: str-replace($color, 'var(');
  $color: str-replace($color, ')');
  $color-h: var(#{$color+'-h'});
  $color-s: var(#{$color+'-s'});
  $color-l: var(#{$color+'-l'});
  @return hsl($color-h, $color-s, calc(#{$color-l} * #{$lightnessMultiplier}));
}

.component {
  background-color: lightness(var(--color-primary), 1.2);
}

Edit saturation:

@function saturation($color, $saturationMultiplier){
  $color: str-replace($color, 'var(');
  $color: str-replace($color, ')');
  $color-h: var(#{$color+'-h'});
  $color-s: var(#{$color+'-s'});
  $color-l: var(#{$color+'-l'});
  @return hsl($color-h, calc(#{$color-s} * #{$saturationMultiplier}), $color-l);
}

.component {
  background-color: saturation(var(--color-primary), 1.2);
}

Final thoughts #

The primary goal of our framework is simplifying the process of starting a web project. We feel we're moving toward that goal. If you have suggestions on what we could improve, get in touch! Any feedback is welcome.

Project duplicated.