🤩 Access a heap of free courses with a SitePoint account

Advanced CSS Theming with Custom Properties and JavaScript

Ahmed Bouchefra

Throughout this tutorial on CSS theming, we’ll be using CSS custom properties (also known as CSS variables) to implement dynamic themes for a simple HTML page. We’ll create dark and light example themes, then write JavaScript to switch between the two when the user clicks a button.

Just like in typical programming languages, variables are used to hold or store values. In CSS, they’re typically used to store colors, font names, font sizes, length units, etc. They can then be referenced and reused in multiple places in the stylesheet. Most developers refer to “CSS variables”, but the official name is custom properties.

CSS custom properties make it possible to modify variables that can be referenced throughout the stylesheet. Previously, this was only possible with CSS preprocessors such as Sass.

Understanding :root and var()

Before creating our dynamic theming example, let’s understand the essential basics of custom properties.

A custom property is a property whose name starts with two hyphens (—) like --foo. They define variables that can be referenced using var(). Let’s consider this example:

:root {
  --bg-color: #000;
  --text-color: #fff;

Defining custom properties within the :root selector means they are available in the global document space to all elements. :root is a CSS pseudo class which matches the root element of the document — the <html> element. It’s similar to the html selector, but with higher specificity.

You can access the value of a :root custom property anywhere in the document:

div {
  color: var(--text-color);
  background-color: var(--bg-color);

You can also include a fallback value with your CSS variable. For example:

div {
  color: var(--text-color, #000);
  background-color: var(--bg-color, #fff);

If a custom property isn’t defined, their fallback value is used instead.

Defining custom properties inside a CSS selector other than the :root or html selector makes the variable available to matching elements and their children.

CSS Custom Properties vs Preprocessor Variables

CSS pre-processors such as Sass are often used to aid front-end web development. Among the other useful features of preprocessors are variables. But what’s the difference between Sass variables and CSS custom properties?

  • CSS custom properties are natively parsed in modern browsers. Preprocessor variables require compilation into a standard CSS file and all variables are converted to values.
  • Custom properties can be accessed and modified by JavaScript. Preprocessor variables are compiled once and only their final value is available on the client.

Writing a Simple HTML Page

Let’s start by creating a folder for our project:

$ mkdir css-variables-theming

Next, add an index.html inside the project’s folder:

$ cd css-variables-theming
$ touch index.html

And add the following content:

<nav class="navbar">Title</nav>
<div class="container">
    <input type="button" value="Light/Dark" id="toggle-theme" />
    <h2 class="title">What is Lorem Ipsum?</h2>
    <p class="content">Lorem Ipsum is simply dummy text of the printing and typesetting industry...</p>
  Copyright 2018

We are adding a navigation bar using a <nav> tag, a footer, and a container <div> that contains a button (that will be used to switch between light and dark themes) and some dummy Lorem Ipsum text.

Writing Basic CSS for Our HTML Page

Now let’s style our page. In the same file using an inline <style> tag in the <head> add the following CSS styles:

* {
  margin: 0;
  height: 100%;
  height: 100%;
  font-family: -apple-system, BlinkMacSystemFont“Segoe UI”, “Roboto”, “Oxygen”, “Ubuntu”, “Cantarell”,
 “Fira Sans”, “Droid Sans”, “Helvetica Neue”,sans-serif;  
  display: flex;
  flex-direction: column;
  background: hsl(350, 50%, 50%);
  padding: 1.3rem;
  color: hsl(350, 50%, 10%);
  flex: 1;
  background:hsl(350, 50%, 95%);
  padding: 1rem;
  padding: 0.7rem;
  font-size: 0.7rem;
  color: hsl(350, 50%, 50%);
.container h2.title{
  padding: 1rem;
  color: hsl(350, 50%, 20%);
  background: hsl(350, 93%, 88%);
  padding: 1rem;
input[type=button] {
  color:hsl(350, 50%, 20%);
  padding: 0.3rem;
  font-size: 1rem;

CSS3 HSL (Hue, Saturation, Lightness) notation is used to define colors. The hue is the angle on a color circle and the example uses 350 for red. All page colors use differing variations by changing the saturation (percentage of color) and lightness (percentage).

Using HSL allows us to easily try different main colors for the theme by only changing the hue value. We could also use a CSS variable for the hue value and switch the color theme by changing a single value in the stylesheet or dynamically altering it with JavaScript.

This is a screen shot of the page:

Example HTML page

Check out the related pen:

See the Pen CSS Theming 1 by SitePoint (@SitePoint) on CodePen.

Let’s use a CSS variable for holding the value of the hue of all colors in the page. Add a global CSS variable in the :root selector at the top of the <style> tag:

  --main-hue : 350;

Next, we replace all hard-coded 350 values in hsl() colors with the --main-hue variable. For example, this is the nav selector:

  background: hsl(var(--main-hue) , 50%, 50%);
  padding: 1.3rem;
  color: hsl(var(--main-hue), 50%, 10%);

Now if you want to specify any color other than red, you can just assign the corresponding value to --main-hue. These are some examples:

  --red-hue: 360;
  --blue-hue: 240;
  --green-hue: 120;
  --main-hue : var(--red-hue);

We are defining three custom properties for red, blue and green, then assigning the --red-hue variable to --main-hue.

This a screen shot of pages with different values for --main-hue:

Pages with different themes

CSS custom properties offer a couple of benefits:

  • A value can be defined in a single place.
  • That value can be named appropriately to aid maintenance.
  • The value can be dynamically altered using JavaScript. For example, the --main-hue can be set to any value between 0 and 360.

Using JavaScript to dynamically set the value of --main-hue from a set of predefined values or user submitted value for hue (it should be between 0 and 360) we can provide the user with many possibilities for colored themes.

The following line of code will set the value of --main-hue to 240 (blue):

document.documentElement.style.setProperty('--main-hue', 240);

Check out the following pen, which shows a full example that allows you to dynamically switch between red, blue and green colored themes:

See the Pen CSS Theming 2 by SitePoint (@SitePoint) on CodePen.

This is a screen shot of the page from the pen:

A page with different themes

Adding a CSS Dark Theme

Now let’s provide a dark theme for this page. For more control over the colors of different entities, we need to add more variables.

Going through the page’s styles, we can replace all HSL colors in different selectors with variables after defining custom properties for the corresponding colors in :root:

  --nav-bg-color: hsl(var(--main-hue) , 50%, 50%);
  --nav-text-color: hsl(var(--main-hue), 50%, 10%);
  --container-bg-color: hsl(var(--main-hue) , 50%, 95%);
  --content-text-color: hsl(var(--main-hue) , 50%, 50%);
  --title-color: hsl(var(--main-hue) , 50%, 20%);
  --footer-bg-color: hsl(var(--main-hue) , 93%, 88%);
  --button-text-color: hsl(var(--main-hue), 50%, 20%);

Appropriate names for the custom properties have been used. For example, --nav-bg-color refers to the color of the nav background, while --nav-text-color refers to the color of nav foreground/text.

Now duplicate the :root selector with its content, but add a theme attribute with a dark value:


This theme will be activated if a theme attribute with a dark value is added to the <html> element.

We can now play with the values of these variables manually, by reducing the lightness value of the HSL colors to provide a dark theme, or we can use other techniques such as CSS filters like invert() and brightness(), which are commonly used to adjust the rendering of images but can also be used with any other element.

Add the following code to :root[theme='dark']:

:root[theme='dark'] {
  --red-hue: 360;
  --blue-hue: 240;
  --green-hue: 120;
  --main-hue: var(--blue-hue);
  --nav-bg-color: hsl(var(--main-hue), 50%, 90%);
  --nav-text-color: hsl(var(--main-hue), 50%, 10%);
  --container-bg-color: hsl(var(--main-hue), 50%, 95%);
  --content-text-color: hsl(var(--main-hue), 50%, 50%);
  --title-color: hsl(--main-hue, 50%, 20%);
  --footer-bg-color: hsl(var(--main-hue), 93%, 88%);
  --button-text-color: hsl(var(--main-hue), 50%, 20%);
  filter: invert(1) brightness(0.6);

The invert() filter inverts all the colors in the selected elements (every element in this case). The value of inversion can be specified in percentage or number. A value of 100% or 1 will completely invert the hue, saturation, and lightness values of the element.

The brightness() filter makes an element brighter or darker. A value of 0 results in a completely dark element.

The invert() filter makes some elements very bright. These are toned down by setting brightness(0.6).

A dark theme with different degrees of darkness:

A dark theme

Switching Themes with JavaScript

Let’s now use JavaScript to switch between the dark and light themes when a user clicks the Dark/Light button. In index.html add an inline <script> before the closing </body> with the following code:

const toggleBtn = document.querySelector("#toggle-theme");
toggleBtn.addEventListener('click', e => {
  console.log("Switching theme");
    document.documentElement.setAttribute('theme', 'dark');

Document.documentElement refers to the the root DOM Element of the document — that is, <html>. This code checks for the existence of a theme attribute using the .hasAttribute() method and adds the attribute with a dark value if it doesn’t exist, causing the switch to the dark theme. Otherwise, it removes the attribute, which results in switching to the light theme.

Changing CSS Custom Properties with JavaScript

Using JavaScript, we can access custom properties and change their values dynamically. In our example, we hard-coded the brightness value, but it could be dynamically changed. First, add a slider input in the HTML page next to the dark/light button:

 <input type="range" id="darknessSlider" name="darkness" value="1" min="0.3" max="1" step="0.1" />

The slider starts at 1 and allows the user to reduce it to 0.3 in steps of 0.1.

Next, add a custom property for the darkness amount with an initial value of 1 in :root[theme='dark']:

  --theme-darkness: 1;

Change the brightness filter to this custom property instead of the hard-coded value:

filter: invert(1) brightness(var(--theme-darkness));

Finally, add the following code to synchronize the value of --theme-darkness with the slider value:

const darknessSlider = document.querySelector("#darknessSlider");
darknessSlider.addEventListener('change', (e)=>{
  const val = darknessSlider.value
  document.documentElement.style.setProperty('--theme-darkness', val);

We’re listening for the change event of the slider and setting the value of --theme-darkness accordingly using the setProperty() method.

We can also apply the brightness filter to the light theme. Add the --theme-darkness custom property in the top of the :root selector:

  --theme-darkness: 1;

Then add a brightness filter in the bottom of the same selector:

  filter: brightness(var(--theme-darkness));

You can find the code of this example in the following pen:

See the Pen CSS Theming by SitePoint (@SitePoint) on CodePen.

Here’s a screenshot of the dark theme of the final example:

A screen shot of the dark theme

Here’s a screenshot of the light theme:

A screenshot of the light theme


In this tutorial, we’ve seen how to use CSS custom properties to create themes and switch dynamically between them. We’ve used the HSL color scheme, which allows us to specify colors with hue, saturation and lightness values and CSS filters (invert and brightness) to create a dark version of a light theme.

Here are some links for further reading if you want to learn more about CSS theming: