How to set up site theming with CSS variables

CSS Custom Properties, commonly known as CSS variables, have been steadily making their way into browsers for some time now. With the release of Microsoft Edge 15 back in March, they are now supported by all major modern browsers. 

This means that for projects that aren't burdened by having to support legacy browsers such as Internet Explorer, CSS variables can be considered – for all intents and purposes – safe to use.

Many developers already use variables with preprocessors such as Sass or Less as a means of reducing code repetition. Native CSS variables work in largely the same way, but don't require any additional build step, cascade like a regular CSS property, and most importantly, are available at runtime. 

These benefits give native CSS variables an edge over their preprocessor siblings, and opens the door for developers to do a lot of interesting things with a lot less code.

In this tutorial, we'll be exploring how we can use CSS variables to implement website theming into a mock web app called NoteApp. This will involve writing CSS and JavaScript to allow the user to change the colour scheme and toggle the amount of columns shown. We'll then leverage the Web Storage API to make sure any changes are saved for future sessions.

Get the tutorial files

First things first, download the project files from Web Designer's FileSilo. To do this, go to FileSilo, select Free Stuff and Free Content next to the tutorial. Note: First time users will have to register to use FileSilo.

01. Set up the workspace

In the '/website-template' directory you'll find the static NoteApp webpage in which we'll be implementing website theming using CSS variables and the Web Storage API. Open the directory in your preferred text editor.

02. Set up our CSS variables

In 'main.css', you'll find much of the project's CSS. At the top, create a block targeting the 'root' pseudo-class. Within it define variables like in the snippet below. This root block contains global variables that will cascade down through our stylesheet like any regular CSS property.

:root {
  --primary: #2F353E;
  --secondary: #2B9BCA;
  --tertiary: #F3583F;
  --quaternary: #E0E1E2;
  --quinary: #FFFFFF;
}

03. Replace colours with variables

Go through the stylesheet, either manually or using find/replace, and swap any static colour hex codes with their corresponding variable from the root block. To do this, use the var function and pass the variable's name. This tells the browser to reference that variable's value. For example:

.c-header {
  background-color:var(--primary);
  color:var(--quinary);
}

04. Locate inline SVG for icons

Open 'index.html' and one of the first things you notice is a large SVG element. This contains the paths for all the page's icons, each wrapped in a symbol element, and given a unique ID. These symbols are then referenced where needed with the 'use' element, allowing for the reuse of icons without any duplication of SVG code.

05. Add CSS variables to the SVG

One benefit of using inline SVG instead of a .SVG file is that CSS can access its internal structure. This means so can our CSS variables (Except Edge, at the time of writing). In 'index.html', replace the SVG's style block with the snippet below. Now each of the SVG's colours are linked to the values of our CSS variables.

<style>
  .primary {
  fill:var(—primary, #2F353E);
  }
  .secondary {
  fill:var(—secondary, #2B9BCA);
  }
</style>

06. Create a columns variable

We can also use CSS variables for properties other than colour. Create a new variable in the root block, call it 'columns', and give it a value of 3. This should match the default value of the 'Columns' UI component on the page. When functional, this component will toggle the number of columns.

:root {
  --primary: #2F353E;
  --secondary: #2B9BCA;
  --tertiary: #F3583F;
  --quaternary: #E0E1E2;
  --quinary: #FFFFFF;
  --columns: 3;
}

07. Implement a columns toggle

Using our new columns variable and the calc() function, we'll now calculate how wide each note component should be in order to create the correct number of columns. For example, if columns is set to 4, each note should have a width of 25%.

.c-note {
  width:calc(100% / var(--columns));
}

08. Add JS classes and data attributes

When applying JS behaviour to a DOM element, hook into it via a class with a JS prefix. This decouples the functional aspects of an element from CSS ones. Let's add a 'js-update-variable' class to all colour and radio inputs as well as a 'data-attribute' referencing the corresponding variable to update.

<input type="color" value="#2F353E" 
class="js-update-variable u-hidden-visually" 
data-variable="primary">

09. Add JS to update CSS variables

Open 'main.js' and add the snippet below. This loops through all our 'js-update-variable' inputs and adds logic, so on change, the variable referenced in its data-variable attribute is updated with the inputs value. The colour swatches and column toggle should now be working!

var varTrig = document
.querySelectorAll(".js-update-variable");
for(var i = 0; i < varTrig.length; i++){
varTrig[i]
.addEventListener("change", function(){
document.documentElement.style
.setProperty("--" + this.dataset.variable, 
this.value);
});
}

10. Attach a JS class to the Save button

It makes sense to only save the user's colour scheme when they click the save button, as that allows them to experiment with themes as much as they'd like without automatically overriding the current one. To start, add '.js-save-colours' to the 'Save' button to function as our JS hook.

<button class="c-button js-save-colours"
data-modal="js-modal" data-modal
-content="js-colours-modal-content">
Save</button>

11. Create colours array

Back in 'main.js', declare a new variable called 'colours' and assign to it a new array containing all the colour variables we want to be saved once the save button has been clicked.

var colours = [
  "primary",
  "secondary",
  "tertiary",
  "quaternary",
  "quinary"
];

12. Build Save button event listener

Below the colours array, create a click event listener for the 'js-save-colours' class which we previously added to the Save button. Inside it create a new variable called 'htmlStyles' and assign it the computed properties of the root HTML element. We will use this to access our CSS variables.

document.querySelector(".js-save-colours")
.addEventListener("click", function() {
var htmlStyles = window
.getComputedStyle(document
.querySelector("html")),
}),

13. Record CSS colour variables

Next, within the event listener, create a new variable called 'coloursToSave' and assign it an empty object. Next, create a FOR loop using the 'colours' array from step 11. Within it, we'll add a complete key/value record of the variables mentioned in the 'colours' array to the 'coloursToSave' object.

array to the 'coloursToSave' object.
coloursToSave = new Object;
for(var i = 0; i < colours.length; i++) {
coloursToSave[colours[i]] = htmlStyles
.getPropertyValue("--" + colours[i]);
}

14. Send coloursToSave to localStorage

Now we have all the colour variables saved in 'coloursToSave', we'll send it to a component of the Web Storage API called localStorage. This is essentially an object that persists across sessions. We can store other objects within it using its 'setItem()' method. Let's send it our 'coloursToSave' object.

localStorage.setItem("colours",
JSON.stringify(coloursToSave));

15. Add a JS class to Columns

In addition to colours, we also want to make sure that our chosen columns number also persists across sessions. The first step towards this would be to add 'js-update-columns' to all radio inputs within the columns component.

<input type="radio" name="columns" class
="js-update-variable js-update-columns
u-hidden-visually" value="1"
data-variable="columns">

16. Send columns to localStorage

For each 'js-update-columns', we'll next assign event listeners to watch for changes. On detection, we'll then send the current value of the 'columns' variable to localStorage, again using its 'setItem()' method. Unlike with colours, we don't need to stringify this value as it's not an object.

var colInputs = document
.querySelectorAll(".js-update-columns");
for(var i = 0; i < colInputs; i++) {
colInputs[i].addEventListener("change",
function(){ var htmlStyles = window
.getComputedStyle(document
.querySelector("html"));
localStorage.setItem("columns", 
htmlStyles.getPropertyValue("--columns"));
});}

17. Handle new sessions

If the user returns after previously choosing a colour and column preference, we need to build logic so this saved data is turned back into CSS variables. The first step is to define a 'DOMContentLoaded' event listener, and then an 'if' statement to check for any saved data in localStorage using its 'getItem()' method.

document.addEventListener
("DOMContentLoaded",function() {
if(localStorage.getItem("colours")) {
}
if(localStorage.getItem("columns")) {
}
});

18. Amending colour variables

In the colours 'if' statement, create a new variable called 'savedColours' and assign it the value of a parsed colours object from localStorage. Using a FOR IN loop with 'savedColours', grab each colour key/value pair and append it to the root HTML element as a CSS variable. 

var savedColours = JSON.parse
(localStorage.getItem("colours"));
for(colour in savedColours){
document.documentElement.style.setProperty
("--" + colour, savedColours[colour]); }

19. Gather column variables data

Before we can amend the columns variable, we have to first grab references to the saved data in localStorage and also the radio inputs within the 'Columns' component. The latter is so we can updates its state to make sure the correct number is pre-selected.

var columns = localStorage
.getItem("columns"),
columnTriggers = document.
querySelectorAll(".js-update-columns");

20. Amend variable and state

Finally, we'll update the columns CSS variable with its saved localStorage counterpart and set the checked attribute of the relevant radio input within the 'Columns' component to true. The 'columns - 1' is to compensate for the fact that the columns NodeList is zero-based.

document.documentElement.style
.setProperty("--columns", columns);
columnTriggers[columns - 1].checked = true;

21. Test the finished product

The finished product: everything on the mock NoteApp page should now work

That's it! Everything on the mock NoteApp page should now work. You can create your own colour scheme by clicking the swatches and then commit it to localStorage via the Save button. If you wish to toggle the amount of columns shown, click the appropriate number in the columns component.

This article originally appeared in Web Designer magazine issue 264. Buy it here.

Read more: