Programming CSS to perform Sass colour functions

I wrote recently about moving away from Sass to using native CSS features. I had this to say on the topic of mixins in Sass:

These can be very useful, but now there’s a lot that you can do just in CSS with calc(). The built-in darken() and lighten() mixins are handy though when it comes to colours.

I know we will be getting these in the future but we’re not there yet with CSS.

Anyway, I had all this in the back of my mind when I was reading Lea’s excellent feature in this month’s Increment: A user’s guide to CSS variables. She’s written about a really clever technique of combining custom properites with hsl() colour values for creating colour palettes. (See also: Una’s post on dynamic colour theming with pure CSS.)

As so often happens when I’m reading something written by Lea—or seeing her give a talk—light bulbs started popping over my head (my usual response to Lea’s knowledge bombs is either “I didn’t know you could do that!” or “I never thought of doing that!”).

I immediately set about implementing this technique over on The Session. The trick here is to use separate custom properties for the hue, saturation, and lightness parts of hsl() colour values. Then, when you want to lighten or darken the colour—say, on hover—you can update the lightness part.

I’ve made a Codepen to show what I’m doing.

Let’s say I’m styling a button element. I make custom propertes for hsl() values:

button {
  --button-colour-hue: 19;
  --button-colour-saturation: 82%;
  --button-colour-lightness: 38%;
  background-color: hsl(
    var(--button-colour-hue),
    var(--button-colour-saturation),
    var(--button-colour-lightness)
  );
}

For my buttons, I want the borders to be slightly darker than the background colour. When I was using Sass, I used the darken() function to this. Now I use calc(). Here’s how I make the borders 10% darker:

border-color: hsl(
  var(--button-colour-hue),
  var(--button-colour-saturation),
  calc(var(--button-colour-lightness) - 10%)
);

That calc() function is substracting a percentage from a percentage: 38% minus 10% in this case. The borders will have a lightness of 28%.

I make the bottom border even darker and the top border lighter to give a feeling of depth.

On The Session there’s a “cancel” button style that’s deep red.

Here’s how I set its colour:

.cancel {
  --button-colour-hue: 0;
  --button-colour-saturation: 100%;
  --button-colour-lightness: 40%;
}

That’s it. The existing button declarations take care of assigning the right shades for the border colours.

Here’s another example. Site admins see buttons for some actions only available to them. I want those buttons to have their own colour:

.admin {
  --button-colour-hue: 45;
  --button-colour-saturation: 100%;
  --button-colour-lightness: 40%;
}

You get the idea. It doesn’t matter how many differently-coloured buttons I create, the effect of darkening or lightening their borders is all taken care of.

So it turns out that the lighten() and darken() functions from Sass are available to us in CSS by using a combination of custom properties, hsl(), and calc().

I’m also using this combination to lighten or darken background and border colours on :hover. You can poke around the Codepen if you want to see that in action.

I love seeing the combinatorial power of these different bits of CSS coming together. It really is a remarkably powerful programming language.

Have you published a response to this? :

Responses

blog.jim-nielsen.com

tldr; define your colors with individual hsl values using CSS variables, then compose your color declarations using the individual hsl values while using calc where you want to do the Sass equivalent of saturate, desaturate, lighten, darken, or adjust-hue.

:root { --color-primary-h: 30; --color-primary-s: 100%; --color-primary-l: 50%;
} /* desaturate the primary color */
.element { background-color: hsl( var(--color-primary-h), calc(var(--color-primary-s) - 20%), var(--color-primary-l) );
}

Verbose, but cool! Read on for a more detailed explanation.

Prior Art

I was recently reading Jeremy’s post Sass and Clamp where he talks about moving off Sass because most of the features he needs from Sass, like variables and mixins, are available in some form or fashion in modern CSS.

Mixins. These can be very useful, but now there’s a lot that you can do just in CSS with calc(). The built-in darken() and lighten() mixins are handy though when it comes to colours.

I’ve gone through a similar journey myself of moving off Sass in order to have one less dependency between me and the browser. I’ve felt quite happy as of late with no Sass in any of my personal projects. That said, I have always missed the color functions in Sass. I always loved those. Heck, I built a tool called SassMe which helps you visualize the output of Sass color functions in real time. All of this got me thinking: could you actually do an equivalent of something like Sass’ saturate() in CSS in 2020? Short answer: you can!

(Note: after reading Jeremy’s post mentioned above, I wrote this blog post. While proof-reading before hitting publish, Jeremy quickly followed up with a similar train of thought to what you’ll find here in his post “Programming CSS to perform Sass colour functions”—its worth also checking out.)

There’s been a lot of discussion and work in the area of color functions in CSS. Tyler illustrated the potential of some of this promising work on his site ColorMe which is like SassMe but using color functions from a CSS working draft. It sounds like the particular approach he was illustrating has been abandoned but there’s renewed effort in other areas to bring color functions to CSS natively.

In her article, A User’s Guide to CSS Variables, Lea Verou outlines how you can use CSS variables to generate different shades of color in your stylesheets.

Out of the color syntaxes currently available to CSS, hsl() tends to work better for creating color variations (until we get lch(), which is far superior due to its wider range and perceptual uniformity). If we anticipate needing only lighter/darker and alpha variants, we can use a variable for both hue and saturation

She then gives the following example:

:root { --base-color-hs: 335, 100%; --base-color: hsl(var(--base-color-hs), 50%); --base-color-light: hsl(var(--base-color-hs), 85%); --base-color-dark: hsl(var(--base-color-hs), 20%); --base-color-translucent: hsla(var(--base-color-hs), 50%, .5);
}

She then explains:

We can use these variables throughout our CSS or create new variations on the fly. Yes, there’s still a little duplication—the base color lightness—but if we plan to create many alpha variations, we could create a variable with all three coordinates, or one with the lightness.

As you can see from the above, what I’m presenting here isn’t necessarily new. I think it’s just one more step beyond what Lea proposed above.

Some Background on HSL and Sass

HSL is pretty cool. It has a few problems and, as Lea alluded to, there are better things coming (lch); nonetheless, I think HSL is a great mental model for thinking about color and programmatic control of color. Chris sums it up really well:

Hue isn’t intuitive, but it’s not that weird. You take a trip around the color wheel from 0 to 360. Saturation is more obvious where 0% has all the color sucked out, like grayscale, and 100% is fully rich color at that hue. Lightness is “normal” at 50% and adds white or black as you go toward 100% and 0%, respectively. I’m sure that’s not the correct scientific or technical way of explaining it, but that’s the brain logic.

So HSL is great for manipulating color. I actually learned just how great when I was building SassMe. Want to know the secret to how it works? Under the hood, it essentially takes a HEX color, converts it to HSL, maps one of the Sass color functions to the color value by adding/subtracting the appropriate h, s, or l values, then converts it back to a color for the browser (I built this before HSL existed as a viable option for declaring a color in the browser).

Sass has the following functions, each of which essentially takes a color, puts it in the HSL color space, then adds/subtracts the value as defined by the developer.

  • adjust-hue which adds/subtracts from the h value
  • saturate which adds to the s value
  • desaturate which subtracts from the s value
  • lighten which adds to the l value
  • darken which subtracts from the l value

So if you looked at an implementation of these functions, conceptually you’d see something something like this:

.element { background-color: lighten(#0000ff, 5%);
} /* What is being done in the above? It’s basically: Convert #0000ff to hsl equivalent — hsl(240, 100%, 50%) Adds 5% to the l value — hsl(240, 100%, 55%) Convert it back to hex color — #1a1aff
*/

Maybe you already see where this is going: those particular color functions are merely adding/subtracting values from hsl color values, and we have a way to add/subtract values in CSS with calc()!

Doing Color Functions in CSS

So if you wanted to do the equivalent of Sass’ hsl color functions in CSS, how would you do it?

First, pick a color and define its component hsl parts as independent values using CSS variables. In a real code base you’re likely to have more than one color so you’d want to give your variables good names to tell them apart, like --color-primary-h, but for simplicity’s sake in my example I’m going to just call it --h. Once you have the component hsl color values defined, you can compose them together in an hsl() function in CSS.

:root { --h: 100; --s: 50%; --l: 50%;
} .hsl-element { background-color: hsl(var(--h), var(--s), var(--l));
}

You could drop those in an hsla() too and be able to control the alpha channel on a case-by-case basis if you wanted.

.hsla-element { background-color: hsla(var(--h), var(--s), var(--l), .5);
}

“Ok,” you might say, ”that’s neat and all, but if I just want my base color, having to write out the nested variables inside an hsl() function every time can get tiring.” That’s true. So make an --hsl variable out of your base h, s, and l variables (which you could also mix with hsla()).

:root { --h: 100; --s: 50%; --l: 50%; --hsl: var(--h), var(--s), var(--l);
} .hsl-element { background-color: hsl(var(--hsl));
} .hsla-element { background-color: hsla(var(--hsl), .5);
}

You could take that a step further if you really wanted to and just cut out having to type hsl every time by defining the hsl function in a variable value. Here’s the entirety of these composable pieces:

:root { --h: 100; --s: 50%; --l: 50%; --hsl: var(--h), var(--s), var(--l); --hslf: hsl(var(--hsl));
} .hsl-element { background-color: hsl(var(--h), var(--s), var(--l));
} .hsla-element { background-color: hsla(var(--hsl), .5);
} .hsl-function-element { background-color: var(--hslf);
}

Now you have all the ingredients you need to mix-n-match how you want to declare your colors. This enables you to start using calc() to modify HSL values for your color palette on the fly.

:root { --h: 100; --s: 50%; --l: 50%; --hsl: var(--h), var(--s), var(--l); --hslf: hsl(var(--hsl));
} .normal { background-color: var(--hslf);
} .adjust-hue { background-color: hsl( calc(var(--h) + 100), var(--s), var(--l) );
} .saturate { background-color: hsl( var(--h), calc(var(--s) + 20%), var(--l) );
} .desaturate { background-color: hsl( var(--h), calc(var(--s) - 20%), var(--l) );
} .lighten { background-color: hsl( var(--h), var(--s), calc(var(--l) + 20%) );
} .darken{ background-color: hsl( var(--h), var(--s), calc(var(--l) - 20%) );
}

You can check this all out in action on my codepen and see all the different ways you can play with color in this fashion, from alpha channels:

To Sass color functions, like adjust-hue():

saturate() and desaturate()

lighten() and darken()

What’s really neat about this is that the browser seems to handle min/max on your color calculations for you. For example, if you have a color value like hsl(50, 100%, 50%) and you add 700% to the l value resulting in a value like hsl(50, 100%, 750%), that value gets interpreted by the browser at the l’s max value of 100% (i.e. hsl(50, 100%, 100%)). This is true for the h, s, or l values. This helps you not break color appearances because, for example, you saturated the color too much. It also absolves you from having to leverage min/max in CSS and writing something like min(calc(var(--h) + 100), 360).

Caveats

Obviously the problem with this is naming. I’ve tricked you with using short names like --h, --s, and --l. More likely you’re going to have a palette of named colors and each one would have to have these variants. Think functional composition in CSS.

:root { /* primary color / --color-primary-h: 50; --color-primary-s: 50%; --color-primary-l: 50%; --color-primary-hsl: var(--color-primary-h), var(--color-primary-s), var(--color-primary-l); --color-primary-hslf: hsl(var(--color-primary-hsl)); / accent color / --color-accent-h: 38; --color-accent-s: 75%; --color-accent-l: 35%; --color-accent-hsl: var(--color-accent-h), var(--color-accent-s), var(--color-accent-l); --color-accent-hslf: hsl(var(--color-accent-hsl)); / all my other colors here... */
}

That’s a lot of writing. Because of the declarative nature of CSS, you’re never going to get something as terse as what you could get in Sass. So sure, you’re typing more characters. But you know what you’re not doing? Wrangling build plugins and updating dependencies to get Sass to build. What you write gets shipped directly to the browser and works as-is, now and for eternity. It’s hard to say that about your Sass code.

# Saturday, May 30th, 2020 at 7:00pm

2 Likes

# Liked by Oliver Ash on Saturday, May 30th, 2020 at 2:15pm

# Liked by George Salib® on Saturday, May 30th, 2020 at 2:15pm

Previously on this day

1 year ago I wrote Indie web events in Brighton

Homebrew Website Club every Thursday, and Indie Web Camp on October 19th and 20th.

2 years ago I wrote The Gęsiówka Story

Republishing a forgotten piece of history.

3 years ago I wrote Checking in at Indie Web Camp Nuremberg

Posting from Swarm to my own site.

4 years ago I wrote Regression toward being mean

I need to get better at balance.

5 years ago I wrote 100 words 069

Day sixty nine.

8 years ago I wrote dConstructickets

Get ‘em while they’re hot.

9 years ago I wrote Hashcloud

The web is agreement.

14 years ago I wrote Copenhagen

I’m off to Denmark for the Reboot conference.

17 years ago I wrote Laptop Land

As promised, I’m blogging wirelessly from Riki Tik’s in the North Laine, Brighton.

17 years ago I wrote Switching lifestyles

Mark Frauenfelder is making another switch.

18 years ago I wrote Too busy to blog

I’m afraid updates are going to be scarce over the next few days. My mother is here in Brighton for a visit so Jessica and I are showing her the sights.