Managing Color Values with Sass

James Steinbach
Share

There are plenty of suggestions for managing color values with Sass. You can generate palettes programatically, rely on Sass color functions, name variables well, or even let math pick all your colors for you.

However, what if all you want to do is choose a group of base colors and use the same dark/light/transparent variations for each of those colors? We can do that pretty easily with a couple of Sass maps and a single function.

A Map of Colors

First, we’re going to start by storing all our our main colors in a map (AKA, a Sass array).

$colors: (
red: #ff4136,
blue: #0074d9,
yellow: #ffdc00,
orange: #ff851b
);

Since Sass 3.3, SassScript gives us several functions to access data in a map. The functions we’ll be using later are map-has-keys() and map-get().

A Map of Variations

Now we’ll create a map to store the variations we need to make from each of the main colors. The $variations map will hold that info. We’re going to use the ability of storing maps inside of maps to organize this well. Let’s take a look at how we’ll organize this information.

First, each color variation’s name is the shorthand we’ll use to describe this color change and call it from our function later. That name is the key for a map with two values: the name of the Sass Script color function and the parameters that the function needs. The value of function must match a function from the RGB functions, HSL functions, or Opacity functions lists.

The value of parameters should be any parameters that the function needs in addition to the original color. If you’ll look at the example $variations map, you’ll see that lighten needs two parameters: an original color (which we’ll get from $colors) and an amount for the change which we’ll store here in parameters. On the other hand, grayscale only needs a starting color, so we don’t need to include parameters for that nested map at all. The mix function called by “shade” takes up to three arguments: starting color, mix color, and percentage for how much of the “mixture” should be the first color. We’ve simply included both additional parameters in a space-separated list.

$variations: (
light: (
function: lighten,
parameters: 15%
),
dark: (
function: darken,
parameters: 10%
),
fade: (
function: rgba,
parameters: .7
),
gray: (
function: grayscale
),
shade: (
function: mix,
parameters: white 80%
)
);

A Function to Generate Colors

Now, the fun part: a single function that gets a color, makes a variation, and then returns the correct value for you to use. First, let’s take a look at the entire function. We will then walk through it one step at a time.

@function color-variation($color, $variation: false) {

@if map-has-key($colors, $color) {
$color: map-get($colors, $color);
} @else {
@if type-of($color) != color {
@error "Invalid color name: `#{$color}`.";
}
}

@if $variation {
@if not map-has-key($variations, $variation) {
@error "Invalid $variation: `#{$variation}`.";
} @else {
$this-variation: map-get($variations, $variation);
$args: join(map-get($this-variation, function), $color);
@if map-get($this-variation, parameters) {
$args: join($args, map-get($this-variation, parameters));
}
@return call($args...);
}
}
@return $color;
}

First you should notice that the function takes two arguments. The name of a color (which will probably be a key from $colors) is required (yes, I said “probably” – more on that soon) and the name of a variation (which must match a key from $variations) is optional.

Validating the Color Value

@if map-has-key($colors, $color) {
$color: map-get($colors, $color);
} @else {
@if type-of($color) != color {
@error "Invalid color name: `#{$color}`.";
}
}

The first @if statement ensures that we have a valid color value to work with. If the string passed as the color is in $colors, the appropriate value from the map will be used. But what if you pass something that isn’t in $colors? The @else half of this operation handles that situation.

To start, it tests whether the $color argument is a valid CSS color or not. If $color is not a valid color (we already know it’s not in $colors), we’ll throw a compiler @error and let you know that you need to use a valid color. On the other hand, if your $color is itself a valid color value (HTML color name, RGB(A), or HSL), we’ll just go ahead and use that value. This gives our color-variation() function a broader reach. For example, you can use it for a one-off color value (sparingly, of course – if you’re repeating a color value, it should probably be in the map).

Note: if you want to force yourself or other developers to stick with the values in the $colors map, just remove two lines: @if type-of($color) != color { and its closing } two lines lower. Leave the @error statement inside the @else block it’s in.

Now that we have a valid color, let’s apply the right variation to it.

Validating the Variation Name

@if $variation {
@if not map-has-key($variations, $variation) {
@error "Invalid $variation: `#{$variation}`.";
} @else {
//...
}
}

The first @if statement checks whether or not we’ve passed a variation name at all. After all, this function will need to return an unchanged color value, right? Right. In your Sass, you’ll use color: color-variation(orange); to get one of the defined colors in a CSS property value.

Next, we use the map-has-key() function to make sure that the variation name we’re passing is actually in the $variations map. If not, we’ll explicitly throw an @error to the compiler. This will provide immediate feedback that we’ve made a mistake and will force us to fix it before we move on.

Note: If you want permission to make mistakes now and fix them later, replace @error with @warn. @warn will send your compiler a “softer” error message and this function will simply return the color value unchanged.

Changing the Color

$this-variation: map-get($variations, $variation);
$args: join(map-get($this-variation, function), $color);

@if map-get($this-variation, parameters) {
$args: join($args, map-get($this-variation, parameters));
}

Here, what we do is get the data assigned to our current variation and assign it to a new variable. Remember, the $variations map holds several other maps, so this step makes it easier to deal with the nested key/value pairs for the current variation.

We’re going to add $args now as well. We’ll eventually need to pass a list of values (function name, color, variation arguments) to the call() function and $args will store that list. We’ll start by using join() to combine the function name of the current variation (map-get($this-variation, function)) with the $color value we validated above (we could use append() instead of join here).

Next, our function checks whether there is a parameters key/value pair for $this-variation. If not, it proceeds to the call() statement. But if there are parameters, we add to the $args list by joining the value/list in the current parameters to it. Now we’ve got the list we need: function name, starting color, and additional arguments for the function.

@return call($args...);

This final line returns the results of passing our $args list to the call() function. call() dynamically generates and runs a function. The first parameter it takes is the function name and any additional parameters are passed to that function as its arguments.

Conclusion

To use this function in your own projects, create your own map of color names and values, a map of color functions and parameters, and use the color-variation() function to get the color value you need in your Sass partials.

Bonus: if you don’t like typing “color-variation()” over and over, use an alias of cv() as a shorthand function:

@function cv($color, $variation:false) {
@return color-variation($color, $variation);
}

If you want to see all of the code for this then check out the source in this Sassmeister gist.