Theming in SASS
Theming a web application can be a daunting task. This tutorial provides ideas and solutions to get a theme to meet your needs.
Theming is the bane of many developers because there’s always many variables to consider, including how those themes will get updated, implementing standards, how to make it easy to use, etc. It always seems like the more you dive into it, the more complex it becomes.
I’ve built many applications that use theming and wanted to share some insights that I’ve learned with different ways to theme. This article will focus on how I accomplished my latest theming endeavour in SASS, but the principles can be applied to other languages/tools as well.
Let’s first look at some of the requirements:
- The theming should be easy to understand (at least all the high level parts)
- The implementation of said theming by developers should be easy to use
- The theme setup should have a set of standardized colors that can be used for many theme attributes
- The theming should allow multiple themes, as well as user customization on themes
- For this example, we’ll just use colors, but you could apply the principles here to include other things like font sizes, standard padding sizes, etc.
To further explain requirements, we’re looking for a very flexible system, but that can enforce certain usage. In my theming, I want to have each theme immediately available at the flip of a switch, so this will be bound to a class name on the document’s html element. Something like so:
<html class="light">...</html>
Toggle the Theme
So, let’s get started! Firstly, let’s get the class part out of the way. There’s many of ways to do this, but for a super simple example, we can just say we’re toggling between a light mode and dark mode when clicking a button. We’ll just use a bit of JSX/jQuery throughout the article for readability. Keep in mind, this is way over-simplified and would normally account for other things as well.
<button
className="myButton"
onClick={this.toggleThemeType}
>
Toggle Light/Dark Theme
</button>---toggleThemeType = () => {
$('html').toggleClass('light').toggleClass('dark');
}
Cool, we have a webpage that will toggle between a dark and light theme via the html class.
This is an important note! You also should add the theme you want as the default on the html element. You could do this either directly in your index.html file, or when your app initializes. Something like so:
$('html').addClass('light');
Implement the Theme Variable in SASS
Now let’s start looking at the SASS setup. I’m not going to go over how to setup and process scss files — that’s a whole other discussion. Let’s assume we have it all working with a webpack setup, we’ve maybe got css-modules enabled (if not, see the comment in one of the mixins later on), and the styles are styling things on the webpage. In my scss file, let’s say I’ve styled .myButton
from the previous example above and we want it to have different border color based on the html theme toggling we set up.
There’s a lot of ways to accomplish this. For this article, here’s what I’ve gone with:
@import '../colors';.myButton {
@include theme(border, borderDark, 1px solid);
@include theme(background-color, backgroundLight);
@include theme(color, textDark); ...other styling stuff
}
A couple things here:
- we’re importing a colors file — more on that later, but that will contain the theme mixin and all the theme generating logic.
- A “theme” mixin that accepts 2 arguments, with an optional third (and fourth) argument. You can gather that these three theme usages will add colors to the given attributes.
Great! This is how simple our theming for any attribute will be to implement. We can apply this mixin to any type of property we want, we just need to remember the mixin call name and parameters: theme(attribute, themeVariable, extras)
One other thing we need is a mixin call that will actually generate all our theme variables. This is optional, but required if your users will customize their own theme variables. I put this in my root scss file for the whole application:
@import './colors';html {
@include generateThemeVariables;
height: 100%;
}
Boom! After we have this, we literally have setup everything a front-end developer would need to know to get going to start using these theme variables.
Setup for Generating the Themes
Let’s get into the nitty gritty for generating the theme variables and the mixins we’re using above. Before we can do that, we need to define our themes!
There’s a lot of arguments on how to define theme-type variables. Some say to use specific naming for where they are used, some say to just do “primary/secondary/etc”, and much more. For my scheme, I chose to a) list out the raw theme colors as unique names, then b) create theme variables based on those colors.
For the raw theme colors, I have a file called colorDefinitions.scss that contains the colors, like so:
$colors: (
tronLightBlue: #a1fefe,
tronDarkBlue: #004242,
white: #FFFFFF,
black: #000000
);
I’m just generating unique names based on this site: http://chir.ag/projects/name-that-color/, but you can do it however you’d like.
Next, we will have a theme definition file for each theme we want to use. Here’s my light.scss file:
@import './functions';$light: (
borderDark: color(tronDarkBlue),
borderLight: color(tronLightBlue),
textDark: color(black),
textLight: color(white),
backgroundDark: color(tronDarkBlue),
backgroundLight: color(tronLightBlue)
);
And my dark.scss file:
@import './functions';$dark: (
borderDark: color(tronLightBlue),
borderLight: color(tronDarkBlue),
textDark: color(white),
textLight: color(black),
backgroundDark: color(tronLightBlue),
backgroundLight: color(tronDarkBlue)
);
You’ll notice a couple things in these theme files:
- I’m importing a functions file. This will contain some useful functions (like “color”) that I’ll discuss more below.
- The variable names in each theme are the same. This is crucial!
- The colors are opposite in each theme. Since we are flipping between a dark and light theme, I wanted the two themes to be opposite each other. It doesn’t have to be this way if I added a third theme that is completely unique. After all, these just correlate to the html class name we set up at the very start.
And let’s add that functions file now:
@import './colorDefinitions';//gets a color value from color map, defined in colorDefinitions
@function color($colorName) {
@return map-get($colors, $colorName);
}//helper function for checking if theme exists in theme colors
@function themeExists($key, $theme) {
@if map-has-key($themeValues, $key) {
@return map-has-key(map-get($themeValues, $key), $theme);
}
@return null;
}//helper function for getting a theme color
@function getThemeValue($key, $theme) {
@return map-get(map-get($themeValues, $key), $theme);
}
You can see our color function we used previously is nothing more than something that gets the actual color from our $colors map, based on a provided color name. We also added a couple helper functions we’ll use later that help us check if a theme exists and for getting a theme’s color.
Quick Recap So Far…
Let’s look at what we have so far:
- themes on the html element via the class name, and we generate theme variables at the html level in the scss file
- a button that will use the theme styles and the button also toggles the theme.
- a color definition file with unique colors
- two theme files that define variables and what colors they use.
- a functions file with a couple helper functions
At this point, you may be asking why this is so complicated already! If you remember our original goals, we wanted theming to be easy for general use and consistent. By adding all this extra complexity up front, we’ll find that actually using the theme variables is incredibly easy to the point where another developer only needs to know the theme mixin and the theme variable names in order to get fully going.
But we’re not quite there yet! We have all the groundwork laid out, so now we just need to generate the themes that sass will use.
Generating the Themes
Let’s make a mixins file now. This is by far the most complex thing, so I’ll put comments throughout the code below. You may want to copy/paste this into a text editor with scss highlighting for readability.
@import './functions';/*
@mixin theme($property, $key, $inlineExtra:null, $postExtra:null)
Assigns a property a theme value for each defined theme.Example usage:
.mySelector {
padding: 6px;
@include theme(background-color, backgroundLight);
@include theme(border, borderDark, 1px solid);
}sass generated equivalent:
.mySelector {
padding: 6px; :global(.light) & {
border: 1px solid color(woodsmoke);
border: 1px solid var(--theme-light-borderDark, color(woodsmoke));
}
:global(.light) & {
background-color: color(alabaster);
background-color: var(--theme-light-backgroundLight, color(alabaster));
} :global(.dark) & {
border: 1px solid color(alabaster);
border: 1px solid var(--theme-dark-borderDark, color(alabaster));
}
:global(.dark) & {
background-color: color(woodsmoke);
background-color: var(--theme-dark-backgroundLight, color(woodsmoke));
}
}browser output:
.mySelector {
padding: 6px;
}
.light .mySelector {
border: 1px solid #141519;
border: 1px solid var(--theme-light-borderDark, #141519);
}
.light .mySelector {
background-color: #FCFCFC;
background-color: var(--theme-light-backgroundLight, #FCFCFC);
} .dark .mySelector {
border: 1px solid #FCFCFC;
border: 1px solid var(--theme-dark-borderDark, #FCFCFC);
}
.dark .mySelector {
background-color: #141519;
background-color: var(--theme-dark-backgroundLight, #141519);
}
*/
@mixin theme($property, $key, $inlineExtra:null, $postExtra:null) {
@each $theme in $themes {
@if (themeExists($key, $theme)) {
$value: getThemeValue($key, $theme); :global(.#{$theme}) & {
// @at-root .#{$theme} #{&} { // if you aren't using css-modules, use this instead of the :global line above
#{$property}: #{$inlineExtra} #{$value} #{$postExtra}; //fallback for browsers that shouldn't exist anymore
#{$property}: #{$inlineExtra} var(--theme-#{$theme}-#{$key}, #{$value}) #{$postExtra};
}
} @else {
@error "Property #{$key} doesn't exist in #{$theme}";
}
}
}/*
@mixin generateThemeMappings(themeName: string, themeMap: map)
helper function for generating list of theme variables and adding to existing map.
This will add a new theme to the themes array and the theme color list.
*/
@mixin generateThemeMappings($themeName, $newThemeMap) {
//creates/adds to list of theme names
$themes: append($themes, $themeName, $separator: comma) !global; @each $key, $value in $newThemeMap {
//adds new themeKey if doesn't exist
@if not map-has-key($themeValues, $key) {
$themeValues: map-merge($themeValues, ($key: ( $themeName: $value ))) !global;
}
//adds to existing key map
@else {
$existingKeyMap: map-get($themeValues, $key);
//if theme variable doesn't exist, add it
@if not map-get($existingKeyMap, $themeName) {
$newKeyMap: map-merge($existingKeyMap, ( $themeName: $value ));
$themeValues: map-merge($themeValues, ( $key: $newKeyMap )) !global;
}
}
}
}/*
@mixin generateThemeVariables
Auto-generates the entire list of theme variables for use in var() statements.
Really should only be called in the html selector at the app root.
*/
@mixin generateThemeVariables {
@each $key, $val in $themeValues {
@each $theme in $themes {
@if (themeExists($key, $theme)) {
--theme-#{$theme}-#{$key}: #{getThemeValue($key, $theme)};
}
}
}
}
Whew, that’s a lot. Instead of going over each method here, I’ll go through them as they are used. We’ve already used two of them, so let’s talk about those.
@mixin theme
This is the mixin that developers will use to reference the themes for individual attributes. Basically this does a lot of stuff. When a developer references this mixin, sass will compile it to be a theme level class. So if I had my example from above:
.myButton {
@include theme(border, borderDark, 1px solid);
border-radius: 2px;
}
Our mixin will convert this to ultimately be:
.dark .myButton {
border: 1px solid #a1fefe;
border: 1px solid var(--theme-dark-borderDark, #a1fefe);
}
.light .myButton {
border: 1px solid #004242;
border: 1px solid var(--theme-light-borderDark, #004242);
}
.myButton {
border-radius: 2px;
}
Ah! We can now catch a glimpse into how those global themes will affect our button. This is pretty standard css, so I won’t go over it. However, it is important to note that we not only apply the basic border, we also have the attribute using css variables. While that isn’t necessary for the actual displaying of the themes, what this does allow is for custom user theming overrides for each theme. I’ll talk more about that later.
@mixin generateThemeVariables
This mixin was references in our html scss. Essentially, this will go through each of our themes and generate those css variables mentioned above. The format is ` — theme-<themeName>-<themeVariable>`. Again, if we weren’t using the css variables part, then we wouldn’t need this mixin at all. However, it is necessary if any future user customization is needed.
Finishing the Generating
Alright, now the grand finale…at least on the setup! Let’s make our colors.scss file we referenced earlier.
@import './mixins';/** this section generates a theme map for each theme **/
$themes: ();
$themeValues: ();@import './light';
@include generateThemeMappings(light, $light);@import './dark';
@include generateThemeMappings(dark, $dark);
/** end theme generation **/
This file will actually generate all our theme colors. We setup an import to get each theme, then we actually generate the theme. This leads to the third mixin.
@mixin generateThemeMappings
This takes the name of the theme to generate and the map of variables from the theme. It essentially goes through each theme and each theme variable and zips them into one giant map. Remember how we had the same variable names for both the light and dark theme? This is why. The output of this mixin creates:
$themes: (light, dark);
$themeValues: (
borderDark: (light: #004242, dark: #a1fefe),
...
);
The way the @mixin theme
was written, it utilizes this final map to know what color should be output for each theme. We use our getThemeValue
function that will get the appropriate value from this map.
Got It!
If everything was setup correctly, you should now have a button that switches styles based on the theme! We should never need to worry about these various theme files again because it is all standardized at this point. Let’s look at all the benefits we now have:
- we only need to utilize that theme mixin to get functioning theming.
- You might also notice how incredibly easy it is to add a new theme. You only would need to add a new theme file, and generate the mappings in the colors.scss file.
- If my company decides the rebrand their colors, I only have to change the definitions in the colorDefinitions file, instead of anywhere else.
- We now have a standard list of variables and colors, so we can hand those off to UX/UI to use so they are using the same set as us.
Here’s a final example of what we have now: https://codesandbox.io/s/scss-theming-bebm7
About Those CSS Variables
Remember those css variables I mentioned? Up to this point, they are completely unnecessary. However, they are incredibly useful if we want users to customize their own theme colors for each theme.
Let’s say I wanted to keep the dark and light themes, but let the users customize each to match their preferences, or maybe branding for a client. Because each variable is added at the html level like so: ` — theme-dark-borderDark`, we can just save the user’s preference for custom colors, then define these same variables at a lower level than the html.
How you add those variables per user to the dom is up to you. Here’s a sample function I did at the root of my App.jsx file:
setTheme() {
/* This method should be called to initialize, update, or remove user custom theme variables.
It essentially adds a new style script in the head of the page (thus lower than the html
defaults), which in turn applies the new styles to all theme variables
*/this.themeActive = this.hasTheming;
// if doesn't have theming available and it was present before, remove it
if(!this.hasTheming && this.themeStylesEl) {
this.themeStylesEl.remove();
} else {
this.currentTheme = this.themeVariables;
let el = this.themeStylesEl ? this.themeStylesEl : document.createElement('style');
if(!this.themeStylesEl) {
this.themeStylesEl = el;
document.head.appendChild(this.themeStylesEl);
}
let themeVars = [];
Object.keys(this.themeVariables).map(theme => {
Object.keys(this.themeVariables[theme]).map(variable => {
let value = this.themeVariables[theme][variable];
themeVars.push(`--theme-${theme}-${variable}: ${value};`);
});
});
this.themeStylesEl.innerHTML = `
html {
${themeVars.join('\n')}
}
`;
}
}
In that same code sandbox above, I added a text box that demonstrates this overriding concept. Here’s that same link again: https://codesandbox.io/s/scss-theming-bebm7
Final Thoughts
Hopefully this tutorial gives you ideas for your own theming. There’s many ways to accomplish theming, but if you can make it robust enough, you can save a lot of refactoring headaches down the road.
This is just one solution that only focuses on colors, but you can expand to handle other theming variables as well. You can also expand your theme files to include new themes, like holiday based!