Sass Theming: The Neverending Story
Building grid systems and theming engines is a neverending Sass story. There are probably as many Sass-powered grid systems as JavaScript frameworks nowadays, and pretty much everyone has their own way to deal with color themes in their stylesheets.
In today’s article, I’d like to tackle the latter issue and have a quick round-up of a couple of ways of building color schemes with Sass. But before going any further, what exactly are we talking about?
Let’s say we have a component, such as the classic media object. Now let’s say that we have different color schemes for this component, for instance a palette per category. For a category A, our object would have a red headline and a red-ish border, and for a category B, our object would have a more blue-ish color palette.
Finally, say you don’t have 2 themes but a dozen as well as a lot of color variations within the component. You could concede that managing this by hand would be highly impractical, and this is definitely the kind of thing you want to automate with a tool. In our case, that tool is Sass.
Okay, now we are ready. Although categories are kind of boring so let’s bring in some unicorns and dragons. Any of our approaches will rely on a map of themes. Each theme is a sub-map containing keys mapped to actual colors.
$themes: (
'unicorn': (
'primary': hotpink,
'secondary': pink
),
'dragon': (
'primary': firebrick,
'secondary': red
)
) !default;
Each of our themes consists of two colors: a primary one and a secondary one. We could have named them differently like alpha and beta, it does not matter. The point is only to be able to get “the main color from a theme” for instance.
How does the theming actually work?
There are different ways of course but usually, a theme is nothing but a class to which is bound some specific styles. This class can either be added to the body
element to wrap the whole pages, or to specific components to themify a small module only, as we had considered just before.
All in all, in your stylesheet you will probably expect any of those two variations:
.theme-class .component {
/* Style for the component when child of `.theme-class` */
}
.component.theme-class {
/* Style for the component when has `.theme-class` */
}
The individual mixins approach
Let’s start with a simple one, and probably my favorite: the individual mixins approach. To put it simply, you have a couple of mixins named after the property they intend to styles. For instance, theme-color
to themify the color or theme-background-color
to themify the background.
Using those mixins will probably look like this:
/**
* Media object
* 1. Make the border-color use the secondary color of the theme
*/
.media {
margin: 15px;
padding: 15px 0;
border-top: 5px solid;
float: left;
@include border-color('secondary'); /* 1 */
}
/**
* Media title
* 1. Make the heading color use the primary color of the theme
*/
.media__title {
font-size: 1em;
margin: 0 0 10px;
@include color('primary'); /* 1 */
}
You have to agree this is quite elegant. Moreover, I feel like it is obvious, even without the comments.
Code
Let’s have a look at how to build this. We are not going to repeat the code logic inside each of those individual mixins, so we need a private mixin to gather it all and then be used inside each subsequent mixin.
/// Themify mixin
/// @access private
/// @author Hugo Giraudel
/// @param {String} $property - Property to themify
/// @param {String} $key - Key color to use from theme
/// @param {Map} $themes [$themes] - Map of themes to use
@mixin themify($property, $key, $themes: $themes) {
// Iterate over the themes
@each $theme, $colors in $themes {
// Create a selector (e.g. `.media.theme-unicorn, .theme-unicorn .media`)
&.theme-#{$theme},
.theme-#{$theme} & {
// Output the declaration
#{$property}: map-get($colors, $key);
}
}
}
And now, or public API:
/// Shorthand to themify color through `themify` mixin
/// @access public
/// @see {mixin} themify
@mixin color($arguments...) {
@include themify('color', $arguments...);
}
/// Shorthand to themify border-color through `themify` mixin
/// @access public
/// @see {mixin} themify
@mixin border-color($arguments...) {
@include themify('border-color', $arguments...);
}
/// Shorthand to themify background-color through `themify` mixin
/// @access public
/// @see {mixin} themify
@mixin background-color($arguments...) {
@include themify('background-color', $arguments...);
}
That’s it. Coming back to our previous example, here is what the CSS output would look like:
.media {
margin: 15px;
padding: 15px 0;
border-top: 5px solid;
float: left;
}
.media.theme-unicorn,
.theme-unicorn .media {
border-color: pink;
}
.media.theme-dragon,
.theme-dragon .media {
border-color: red;
}
.media__title {
font-size: 1em;
margin: 0 0 10px;
}
.media__title.theme-unicorn,
.theme-unicorn .media__title {
color: hotpink;
}
.media__title.theme-dragon,
.theme-dragon .media__title {
color: firebrick;
}
Pros
- Thanks to property-named mixins, the API is both clean and clear, even to a non-experienced developer.
Cons
- This approach involves several mixins instead of one which might or might not be considered as extra complexity, even if most of them are just clones.
- Because the colors are directly fetched from the color map based on their key, it is not possible to manipulate them with color functions such as
lighten
ormix
. It might be doable with an extended version of the mixin though.
The block mixin approach
The block mixin approach uses a single mixin instead of several one and relies heavily on the usage of the @content
directive. Using it would look something like:
/**
* Media object
* 1. Make the border-color use the secondary color of the theme
*/
.media {
margin: 15px;
padding: 15px 0;
border-top: 5px solid;
float: left;
@include themify {
border-color: $color-secondary; /* 1 */
}
}
/**
* Media title
* 1. Make the heading color use the primary color of the theme
*/
.media__title {
font-size: 1em;
margin: 0 0 10px;
@include themify {
color: $color-primary; /* 1 */
}
}
Code
The idea is simple: exposing the two colors as variables inside the themify
mixin. The problem is that we cannot actually do this in a clean way. Variables defined in a mixin are not accessible to the content passed through @content
as per the documentation:
The block of content passed to a mixin are evaluated in the scope where the block is defined, not in the scope of the mixin. This means that variables local to the mixin cannot be used within the passed style block and variables will resolve to the global value.
Because of this limitation, we have to hack our way around it. The work-around is actually not that complicated: before outputting @content
, we define one global variable per color in the theme, and after outputting @content
we unset those variables. This way, they are only accessible in the themify
call.
// Initialize our variables as `null` so that when used outside of `themify`,
// they actually output nothing.
$color-primary: null;
$color-secondary: null;
/// Themify mixin
/// @author Hugo Giraudel
/// @param {Map} $themes [$themes] - Map of themes to use
@mixin themify($themes: $themes) {
// Iterate over the themes
@each $theme, $colors in $themes {
// Create a selector (e.g. `.media.theme-unicorn, .theme-unicorn .media`)
&.theme-#{$theme},
.theme-#{$theme} & {
// Set the theme variables with `!global`
$color-primary: map-get($colors, 'primary') !global;
$color-secondary: map-get($colors, 'secondary') !global;
// Output user content
@content;
// Unset the theme variables with `!global`
$color-primary: null !global;
$color-secondary: null !global;
}
}
}
Our previous example would look exactly as with the first approach:
.media {
margin: 15px;
padding: 15px 0;
border-top: 5px solid;
float: left;
}
.media.theme-unicorn,
.theme-unicorn .media {
border-color: pink;
}
.media.theme-dragon,
.theme-dragon .media {
border-color: red;
}
.media__title {
font-size: 1em;
margin: 0 0 10px;
}
.media__title.theme-unicorn,
.theme-unicorn .media__title {
color: hotpink;
}
.media__title.theme-dragon,
.theme-dragon .media__title {
color: firebrick;
}
Pros
- Contrary to the individual mixins solution, the block mixin gives the ability to manipulate colors with functions since we have direct access to the colors stored in theme variables.
- I feel like the API is still quick clear with this approach, especially since we use actual CSS declarations inside the mixin call, which might be easier to understand for some people.
Cons
- The use of global variables in such a way is kind of hacky I must say. No big deal, but code quality is not really the plus side here.
The big ol’ theme mixin approach
This approach is actually something I have already written about here at SitePoint, in this article from last year. The idea is that you have a big ol’ mixin you update every time you need something to be themified.
This means that this mixin is the same for all components across the project. If you want to make this new component themified in some way, you need to open the file where the themify
mixin lives and add a couple of extra rules in there.
// Somewhere in the project, the `themify` mixin
@mixin themify($theme, $colors) {
// See `Code` section
} @include themify;
/**
* Media object
*/
.media {
margin: 15px;
padding: 15px 0;
border-top: 5px solid;
float: left;
}
/**
* Media title
*/
.media__title {
font-size: 1em;
margin: 0 0 10px;
}
@each $theme, $colors in $themes {
@include themify($theme, $colors);
}
Code
/// Themify mixin
/// @author Hugo Giraudel
/// @param {String} $theme - Theme to print
/// @param {Map} $colors - Theme colors
@mixin themify($theme, $colors) {
// Output a theme selector
.theme-#{$theme} {
// Create the two variations of our selector
// e.g. `.theme .component, .theme.component`
.media,
&.media {
border-color: map-get($colors, 'primary');
}
.media__title
&.media__title {
color: map-get($colors, 'secondary');
}
}
}
Pros
- Since the mixin content is manually maintained, it gives a high flexibility with selectors. You can pretty much do whatever you want in there.
- For the same reason, it also gives the ability to manipulate colors with functions like
darken
.
Cons
- Because everything themed is stored in this mixin’s content, it is not well suited for a modular approach. It is probably okay on small to medium projects with global stylesheets though.
- It might be confusing to have component styles divided into several places, i.e. the module stylesheet and the theme mixin.
The class approach
The class approach is actually a DOM-driven one. The idea is that instead of applying specific theme styles from the stylesheet, you instead add theme classes to your markup such as border-color-primary
. These classes, generated with Sass, do nothing on their own but apply some styles when used in combination with our eternal theme-$theme
classes.
You can read more about this system in this article from Harry Roberts.
<div class="media theme-unicorn border-color-primary">
<img class="media__image" src="https://lorempixel.com/100/100" />
<h2 class="media__title color-secondary">This is the headline</h2>
<p class="media__content">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Provident nulla voluptatibus quisquam tenetur quas quidem, repudiandae vel beatae iure odit odio quae.</p>
</div>
// Somewhere in the stylesheet (once)
@mixin themify($themes: $themes) {
// See `Code` section
} @include themify($themes);
/**
* Media object
*/
.media {
margin: 15px;
padding: 15px 0;
border-top: 5px solid;
float: left;
}
/**
* Media title
*/
.media__title {
font-size: 1em;
margin: 0 0 10px;
}
Code
/// Themify mixin
/// @param {Map} $themes [$themes] - Map of themes to use
@mixin themify($themes: $themes) {
// Properties to output, more can be added (e.g. `border-left-color`)
$properties: ('border-color', 'background-color', 'color');
// Iterate over the themes
@each $theme, $colors in $themes {
// Iterate over the colors from the theme
@each $color-name, $color in $colors {
// Iterate over the properties
@each $property in $properties {
// Create a selector
// e.g. `.theme .color-primary, .theme.color-primary`
.theme-#{$theme} .#{$property}-#{$color-name},
.theme-#{$theme}.#{$property}-#{$color-name} {
#{$property}: $color;
}
}
}
}
}
The output of this code would be:
.theme-unicorn .border-color-primary,
.theme-unicorn.border-color-primary {
border-color: hotpink;
}
.theme-unicorn .background-color-primary,
.theme-unicorn.background-color-primary {
background-color: hotpink;
}
.theme-unicorn .color-primary,
.theme-unicorn.color-primary {
color: hotpink;
}
.theme-unicorn .border-color-secondary,
.theme-unicorn.border-color-secondary {
border-color: pink;
}
.theme-unicorn .background-color-secondary,
.theme-unicorn.background-color-secondary {
background-color: pink;
}
.theme-unicorn .color-secondary,
.theme-unicorn.color-secondary {
color: pink;
}
.theme-dragon .border-color-primary,
.theme-dragon.border-color-primary {
border-color: firebrick;
}
.theme-dragon .background-color-primary,
.theme-dragon.background-color-primary {
background-color: firebrick;
}
.theme-dragon .color-primary,
.theme-dragon.color-primary {
color: firebrick;
}
.theme-dragon .border-color-secondary,
.theme-dragon.border-color-secondary {
border-color: red;
}
.theme-dragon .background-color-secondary,
.theme-dragon.background-color-secondary {
background-color: red;
}
.theme-dragon .color-secondary,
.theme-dragon.color-secondary {
color: red;
}
It might looks like an awful lot of generated CSS, but this is something you can re-use several times throughout your whole project, so it actually is not that bad.
Pros
- This solution has the benefit of being a DOM-based approach which turns out to be very interesting when having to manipulate themes on the fly with JavaScript. Indeed, it’s only a matter of adding/removing theme classes to/from elements; very handy.
- While the output of the
themify
mixin might look big, it actually is a quite DRY approach since every themed color, border-color, background-color and whatelse is applied through these classes.
Cons
- In some cases, a DOM-based approach might not be the correct way to proceed for instance when the markup is not hand-crafted (CMS, user-generated content…).
Final thoughts
I am sure I forgot maybe a dozen of other ways to apply theme styles with Sass, but I feel like these 4 different versions already cover a good area of the topic. If I get my choice we either go for the individual mixins approach, or the DOM-drive way with classes all the way if we want to go very modular (which is always good).
What about you, how do you do it?