Attempting to corral colors with a Sass function

Eric Bailey

I’ve been experimenting with a Sass color function, and I can’t tell if I love it or hate it.

The idea is to set up a system that allows you better control over your website or web app’s colors. Color management is a tricky problem at the best of times, so I’m always hunting for ways to better wrangle the chaos.

How it works

There are two requisite parts: a Sass map, and the function itself.

Sass maps

For those unfamiliar with Sass maps, they are a structured data type analogous to JSON that allow you to store key-value pairs within a variable. For example, you could have a key of dogs within a variable called $pet-totals. dogs has a value of 2, with 2 being the total number of dogs present:

$pet-totals: (
  cats:    4,
  dogs:    2,
  rabbits: 1,
  turtles: 5
);

I typically make the map that the Sass color function references a partial, as it is a central location that contains all the color values used by your website or web app. While the function and the map could both be included in one partial, I like to keep them separate because of single responsibility concerns (one file to store color, the other to do stuff with it).

You can use hex codes for color values in your Sass map. You can also use rgba, hsla, and even other Sass variables! You can also use Sass color functions, although things can quickly get out of hand with them if you’re not careful.

Here’s a sample of what the color map could be:

$colors: (
  blue:  #007aff,
  green: #4cd964,
  red:   #ff3b30,

  tab: (
    border: hsla(3, 100%, 59%, 0.50),
    link:   #007aff
    type:   #8e8e93
  )
);

Sass maps can nest content as deep as you want, but I limit color options to three levels deep.

Our first level is our initial palette (blue, green, red,). Second, there’s the component level (tab), which is scoped to contain its own color values. Third, we’ll be introducing a theme level,

Themes could be a number of things. Right now Operating System dark modes are pretty in vogue, but this could just as easily be a way to set up a white labeled application. There’s a lot of complexity tied up in themes, so more on them after you get a little bit more context for why things are structured the way they are.

The function itself

In Sass, functions are little pieces of self-contained logic that you can sprinkle into your styling instructions. Unlike mixins, you can declare functions just about anywhere, making them especially ideal for working with CSS shorthand properties that use multiple values.

Here’s the function’s actual code:

@function c(
  $name,
  $theme:null,
  $component:null,
) {
  // Retrieve a palette color value
  @if ($theme == null) and ($component == null) {
    @return map-get($colors, $name);
  }
  // Retrieve a component color value
  @else if ($theme != null) and ($component == null) {
    @return map-get(map-get($colors, $name), $theme);
  }
  // Retrieve a themed component color value
  @else {
    @return map-get(map-get(map-get($colors, $name), $theme), $component);
  }
}

The function, c(), digs into the map stored in the $colors variable, retrieving a color value dependent on how the function’s declaration is structured. I’m using null a bit strategically here, in that the way the logic is structured you don’t have to supply it as an argument for theming if no theme is present (more on this in a bit).

Short note on the function’s name: I’m normally a fan of less terse, highly descriptive names for my functions, to proactively remove ambiguity about their intended purpose. However, in this case I feel comfortable calling this one c(). Its purpose should be able to be easily inferred based on the fact that it only works for, and is associated with color-related things.

Using it

This function is set up to access colors no more than three levels deep. This provides three tiers of choices to work with:

  1. The site’s palette.
  2. An aspect of a component.
  3. A themed aspect of a component.

1. Palette

c() allows us to get purposeful with how we organize our colors. In our map example earlier, we have what I like to call the “palette,” which is just raw color values without any meaning attached to them other than the color they describe. It’s the first level of nested values in our $colors map.

The palette is usually tied to main brand colors, so red could just as easily be something like brand-primary. Its main benefit is to serve as a reference for what values you should expect to be working with further down in the map file. If your color values are deviating from brand standards, there ought to be a good reason for it!

Short note on naming colors: As someone with a fine arts degree, I used to take great pride in picking my color names. It’s not red, it’s garnet. Green? More like malachite, you philistine.

It turns out that’s not such a great idea when working with other people, or when you come back to a project after not touching it for a while. The kind of vocabulary required to discuss color this way isn’t always necessarily present, which adds effort to being able to use it as a team. That’s not great. If it looks mostly blue, call it blue and save the care for something more important.

The palette is nice to have because sometimes there just isn’t any significant meaning behind a color selection, or it makes for a good placeholder value before a refactor can occur. It also lends itself well to making utility CSS classes, an approach to managing styling I grow to appreciate more and more with every passing day:

.u-color-purple {
  color: c(purple);
}

2. Components

Components are codified bits of UI that let people take actions on things. They could be tabs, buttons, modals, inputs, all that good stuff. Components are also a great way to think about constructing interfaces—they’re the actual mechanisms we employ to make a user flow work.

Because of this, we should be striving to codify our colors into components whenever possible. A tab might currently have a red border, but that’s only one rebrand away from being untrue. If we describe our tab’s border color as an aspect of a component, and not a color value, we abstract this problem away and replace it with meaningful intent:

.tab {
  border: 1px solid c(tab, border);
  color: c(tab, type);
  padding: 1rem 3rem;

  a {
    color: c(tab, link);
  }
}

I think this approach also forces you to think about the individual parts that make up a component, and how they piece together. What we’re driving at is a system where both component creation, and its styling, is highly intentional.

The downside with this is that in any mature website, you get a really long colors map. That’s okay! What I’m pushing for is a process where you downplay an emergent behavior in many Sass color systems: variable chaining.

Variable chaining

If you want to get all comp sci about it, variable chaining is what happens when we don’t observe the Law of Demeter. We want to give our abstractions only enough information to do their job, and not have them applied in completely unrelated contexts. A good example of variable chaining would be a scenario where a color variable calls another color variable from a separate file, which in turn is modified by a Sass color function:

$crimson: #a51c30;
// Other color variables

$brand-primary: $crimson;
$color-outline: saturate($brand-primary, 20%);

.component {
  border: 2px solid $color-outline;
// Other component code
}

It’s clever code, but it’s also a lot to unpack in an environment that already demands you juggle a lot of moving parts.

In this example, we’re three steps removed from the actual color value, and then we’re modifying it even further to boost its saturation. This has two effects: it creates unnecessary cognitive overhead and outputs a completely new color not captured anywhere else. It also assumes good behavior on the part of the people maintaining it. There’s nothing stopping someone from using $color-outline on something that isn’t an outline in another component partial because it gives them the output color they’re looking for.

At this point, the question I’m asking is if all this abstraction work is worth it if it leads to variable-chaining? While conceptually similar, a map-based approach structurally disincentivizes this kind of behavior.

With a map-based approach, all our color is contained in a single partial. This makes it easy to do a simple find/replace on a single file to update color values, while simultaneously preserving their intent. If your values are too disparate to easily do a find/replace, it probably signals an opportunity for your design team to get their house in order.

In addition, this approach saves you from having to comb through multiple Sass partials to find where and how variables are getting chained, and whether or not you can safely update them without unintended consequences. This single source of truth saves a lot of time and headaches when it comes to doing refactoring work.

3. Theming

Theming a website is one of the more complicated things you can do with CSS. It requires a good deal of discipline and planning to do well, as well as a solid understanding of how the cascade and inheritance work.

A big part of the undertaking is, you guessed it, controlling your colors. Whether you’re adjusting something as a reaction to a mode switch (say an operating system dark theme), or “reskinning” it to match someone else’s branding, you’re going to need a place to capture these color values.

That place is our $colors map. What we can do is add another level to it to capture theme color values, and only when needed:

$colors: (
  brand-primary:   #6b9cdd,
  brand-secondary: #ffadfa,
  brand-tertiary:  #fed5d8,

  tab (
    background:  #6b9cdd,
    border:      #00e2ff,
    link:        #ffffe7,
    link-hover:  #ffd5d9,
    link-active: #ffadfb,
    type:        #ffffff,

    dark-theme: (
      background: #012a27,
    )
  )
);

In this example, we only need to adjust the color of the tab component’s background for a dark theme, so we carve out a space for it in the map (dark-theme), then provide a new color key-value pair (background/#012a27). Calling this dark theme color to use inside of a prefers-color-scheme media query would then look like this:

.tab {
  color: c(tab, type);
  border: 1px solid c(tab, border);
  padding: 1rem 3rem;

  @media (prefers-color-scheme: dark) {
    background-color: c(tab, dark-theme, background);
  }

  a {
    color: c(tab, link);

    &:hover,
    &:focus {
      color: c(tab, link-hover);
      text-decoration: none;
    }

    &:focus {
      color: c(tab, link-active);
    }
  }
}

The use of null as a default value for the $theme argument, combined with the function’s logic means that if a theme isn’t present, it will pull the component color (c(tab, type)). If the theme keyword is present and valid, it will pull the themed component color instead (c(tab, dark-theme, type).

dark-theme is just a term I made up. It could just as easily be any other authored value, say the name of the white label brand, the name of a skinned theme, or some other operating system interaction mode.

You can also have multiple themes per component, and each theme can contain any number of key-value pairs:

$colors: (
  tab (
    background:  #6b9cdd,
    border:      #00e2ff,
    link:        #ffffe7,
    link-hover:  #ffd5d9,
    link-active: #ffadfb,
    type:        #ffffff,

    dark-theme: (
      type: #ffffff
    ),

    inverted: (
      type: #ffffff
    ),

    high-contrast-mode: (
      border: buttonFace
    ),

    brand-name: (
      background: #e92977,
      border:     #ecc900,
      type:       #ffffef
    ),

    other-brand-name: (
      background: #9e65ed,
      border:     #00ffea,
      type:       #e8fffd
    )
  )
);

Again, this creates a long partial file, but it is all contained in a single source of highly scoped truth. I think it’s also in the nature of the work being done. Components, when authored to be robust and fault-tolerant, have a lot of consideration put into them, and this is reflective of that.

Precedence

You might have noticed we’re making a rule about the way things are structured. In our world, themes can’t exist without components, but components can exist without themes. If you try and use c(tab, dark-theme) you get a Sass compilation error.

This is intentional—theming should only modify what already exists. While functionality can be added, modified, and removed in a theme, it has to reference something that has already been codified. In this way, theming is just another aspect of our single source of truth partial, helping to keep anything related to color all in one spot.

How this doesn’t work

Color management, like so many other technology issues, is secretly a social problem. Because of this, there’s a few ways this approach could all fall apart.

Learning a new trick

While I’m not a fan of variable chaining colors, it is a very common practice to come across. Changing over to use this approach means your team also has to learn a new way of thinking about something that typically isn’t given a lot of attention.

It also means enforcing it, meaning a system where everyone has to keep each other honest and not fall back on old, familiar ways of doing things. Software can help us with this to a point, but it ultimately has to boil down to making sure everyone rows in the same direction.

Extra work

A map-based approach is more repetitive by nature—some may see it as being a stone’s throw away from abandoning any Sass logic and just declaring color values inline. Quite frankly, I’ve seen enough instances of variable chaining completely tangling up mature projects that I think this is the better approach.

Starting fresh

If you’re working on a dedicated product, it becomes a non-trivial effort to refactor your approach to color management. Such an endeavor would probably be placed far after a number of other backlogged feature requests and maintenance concerns.

Unless your website or web app is new or small enough that swapping techniques out is trivial, the only real place where you can try out this approach is with a new project.

Unfamiliarity

There’s a paradoxical aspect to doing (somewhat) advanced work in Sass: styling is typically not given a lot of attention, so Sass’ full capabilities aren’t always understood. And if it’s not understood, it won’t be used.

Sass is an incredibly powerful language in the right hands, but creating something powerful that is also effective requires other people being able to understand it to use it effectively. It’s sort of like the issue with naming your colors described earlier, only I feel this is one of those situations where it is appropriate to cash in your care.

I’ve worked on a lot of websites. Sass maps aren’t too common. Same for map-get, the function that allows us to reach into Sass maps—c() is effectively syntactic sugar for this.

Getting serious about map-get

map-get() has a lot of versatility, a fact that we shouldn’t take for granted. While deep get/set libraries do exist, and definitely can be a huge for working with sophisticated Sass projects, I feel it’s better to keep c() more self-contained.

Another thing we could use map-get() for is to further reinforce using base colors in our components. By breaking base colors and components into two separate maps, we could combine using map-get() and map-merge() as an approach to access base colors (hat tip to my coworker Stephen Hanson for this idea):

$base: (
  blue:  #007aff,
  green: #4cd964,
  red:   #ff3b30,
);

$components: (
  tab: (
    border: hsla(297, 76%, 50%, 0.50),
    link:   map-get($base, blue),
    type:   map-get($base, red)
  )
);

$colors: map-merge($base, $components);

It will soon be obsolete

CSS Custom Properties are a new way to use variables natively in CSS. They also work in Sass.

While CSS does not have the ability to create structured data like a Sass map, there are two killer features CSS Custom Properties have that make them superior to Sass variables. The first is that their values can be updated in real time by JavaScript on the website or webapp they’re used on. The second is that their values can be scoped, meaning we can change them on a per-context basis:

:root {
  --color-tab-link: #007aff;
  // Other CSS Custom property declarations
}
.c-tab {
  color: var(--color-tab-type);
  // Other component declarations

  @media (prefers-color-scheme: dark) {
    --color-tab-link: #ffffff;

    color: var(--color-tab-link)
    // Other component theme tweaks
  }
}

Here, we’re taking the --color-tab-link Custom Property declared in :root and invoking it in our tab component partial. We can then update its value in the context of a dark color scheme media query to make our tab’s type light colored in order to be legible when the background changes to a dark color value. That’s pretty cool.

What’s even cooler is the being able to be updated in real time bit. In the future, we could use JavaScript to update --color-tab-link‘s value dynamically, say proportionate to the amount of available light or your face’s distance from your device.

The only real barrier to using CSS Custom Properties today is for public projects where you have an unknown audience—browsers such as Internet Explorer 11 and Opera Mini don’t support them. If you don’t have those kinds of concerns (and I would be very careful when making that call), I say ditch Sass variables in favor of CSS Custom Properties as soon as possible.

All this said

While what I’ve suggested works for me on some small to mid-sized projects I’ve worked on, it’s impossible to really know how effective this is until it’s put out there for other people to think about and tinker on.

Putting things out there is somewhat an act of vulnerability, but it’s also one of the best parts of working on the web: You have an idea on how to potentially make things better, flesh it out, then put it out there for the world to see. And then, if the conditions are right, it takes on a life of its own.