Dynamic theme with CSS Variables

Jul 15, 2020 · 10 min read

Theming a web application is always a challenge, and it can easily lead to performance issues. CSS pre-processor variables are good, but not enough for dynamic theming. Luckily, CSS variables can help to solve this challenge efficiently.

So what are CSS Variables? How do they help to enable theming? And why using CSS Variables instead of CSS pre-processors? Let's find out.🚀

What are CSS Variables?

CSS Variables - or custom properties - are variables that we use to store and transfer their values to different CSS properties and can be reused throughout a document.

CSS Variables are custom-made, meaning we can give them any name, follows a specific pattern. A CSS variable name starts with a double hyphen —- and follows by the variable name in separated by -, as seen below:


The reason for using double hyphen -— is to avoid accidental name overlapping of reserved CSS properties, and other special properties starting with a single hyphen - like -web-kit .

We can assign the value to it like any regular CSS property, as in the following example:

--default-color: #69bac9;
--font-size-md: 14px;
--font-family: Lato;

Certainly, the assigned value has to be a valid CSS value, hence:

/* OK */
--font-size-md: 14px; 

/* NOK */
--font-size: 14;

Also, CSS variables’ names are case sensitive, so —default-color is different than —Default-color 😉

💡 Tip - I recommend to use lower case to name your custom variables. It helps to avoid unwanted typo mistakes.

How to use CSS Variable?

We use var() function to retrieve and use the value assigned to a CSS variable. var() receives two arguments, in the following syntax:

var(<your-css-variable-name> [, <fallback-values> ]?)

In which, <your-css-variable-name> is the given name of CSS variable, such as -default-color , and fallback-values is optional argument indicating the fallback values in case the given CSS variable is invalid. For example:

/* No fallback */

/* Single fallback value */
var(--default-color, #000)

/* A list of fonts to fallback, separated by a comma "," */
var(--font-family, "Helvetica", "Arial") 

Note that for shorthand values as fallback, comma , is not needed, as in the following example:

/* OK */
var(--font-size-md, 10px 15px)

/* NOK */
var(--font-size-md, 10px, 15px)

We can assign var() to any CSS property in place of normal property value, such as setting a font color:

color: var(--default-color);

Finally, we must define a custom variable and/or using var() within a CSS ruleset, like any CSS property.

/* Define default-color property in body element*/
body {
  --default-color: #69bac9;

/* Assign value of default-color property to font color
of any element has class "heading" */
.heading {
  color: var(--default-color);

Also, since custom variables follow cascade rules, we can always override the value given to a custom variable within an element. According to the rule of specificity, the new value of that custom variable will only apply to that specific element and its descendants. Its parent and sibling elements stay unaffected by this change.

Take the below code, for instance.

body {
  --default-color: #69bac9;

.pink {
  --default-color: #c70085;

* {
  background-color: var(--default-color);

In this example, we override—-default-color for CSS selector .pink to a different color. All of the elements in the HTML document receive the background color value assigned by —default-color. This change results in any element that has class pink , and its nested children will have a different background than the rest of the page.


You can experiment yourself here 👇🏼

Edit css-variables-experiments

You can also combine with calc() function to calculate a new value based on a custom property value. Isn’t it amazing?

That’s basically how CSS variable (or custom variable) works. Next question - how do we define the custom variables for the entire webpage?

Let’s check out the :root selector.

The :root selector

:root , defined in CSS3, is a CSS selector that aims to select the highest level element in the DOM tree.

In the HTML document, html element is always the highest-level DOM element (or the root node). However, in other document formats like SVG and XML, there are different root nodes (svg or xml), so using html as root selector is not sufficient in these cases. Hence with :root , it makes sure to select the right root element in a given document tree, regardless of the markup language.

:root {
 background-color: #69bac9;

/* In HTML document, it is similar to */
html {
 background-color: #69bac9;

And to use the same custom variables for the entire Web application, we define their values inside :root:

:root {
 --default-color: #69bac9;
 --font-size-md: 14px;
 --font-family: Lato;

⚠️ Note that in case both :root and html selectors present in CSS styling, :root wins over html in the level of specificity, as it is pseudo-class selectors and has the same level with a class selector while html is normal element selector.


So far, so good?

Next, let’s go over our main challenge for today - theming, shall we?

Customize the theme with CSS variables

Enable theming with CSS variables is extremely simple and straightforward, with the following steps:

  1. Define a set of global CSS variables in :root, including one set of color for background and font used in dark mode, one set for light mode, and theme variables.
:root {
  --background-dark: #3d1472; /* for dark mode */
  --font-color-dark: #fff; /* for dark mode */
  --background-light: #9f68e7; /* for light mode */
  --font-color-light: #000; /* for light mode */
  --page-font-size: 14px;
  --page-font-family: Avenir;
  --page-action-btn: #c70085;
  --page-font-color: var(--font-color-dark); /* theme color for font */
  --page-background-color: var(--background-dark); /* theme background color*/

Our theme variables are custom properties whose names have the --page- prefix. To keep the dark theme and light theme color palettes separately from the currently selected theme, we use var() to re-assign these custom variables to --page-font-color and --page-background-color accordingly. This way, if you want to modify any specific theme palettes or stylings, you only need to make the changes on the particular theme variables and keep the rest of the code untouched. Also, when you want to extend our themes and have your CSS organized, you can always extract the related styles for a theme to separate CSS file, at the minimum effort.

  1. Once we have the global CSS variables, we can now write the styles for our document's element, using var() and the variables defined in step 1.
* {
  color: var(--page-font-color);
  font-family: var(--page-font-family, Helvetica, Arial, sans-serif);

body {
  background: var(--page-background-color);

.btn {
  background: var(--page-action-btn);
  border: none;
  padding: 5px 10px;
  border-radius: 5px;
  cursor: pointer;

The above code is a simple setup, where we apply the same font color and font family to all elements in the DOM tree. At the same time, body receives the primary background theme color, and any element with class btn gets a slightly different background.

  1. Our page with default Dark mode will look like:


  1. To change from Dark Mode to Light Mode, we call the method style.setProperty of the main body element and set the related CSS properties to take the value of the right theme mode, as seen below:
const backgroundColor = `var(--background-${currentMode})`;
const fontColor = `var(--font-color-${currentMode})`;

document.body.style.setProperty("--page-background-color", backgroundColor);

document.body.style.setProperty("--page-font-color", fontColor);

in which the currentMode indicates what mode we are in dark or light

  1. We can add this logic to an onclick event of the main button of the page, and clicking on the button will toggle the mode accordingly:


  1. Besides, we can enable the run-time theme selector by adding an input field with type:color as a color picker.
<label for="Theme">Choose color theme: </label>
<input type="color" name="Theme" value="#3d1472" id="theme" />
  1. And we add a handler for onchange event of input, which changes the value of --page-background-color to the selected color.
input.onchange = () => {  
  document.body.style.setProperty("--page-background-color", input.value);

And that's how we theme it 🎉. Now user can customize the background theme color to any color he/she prefers 💪!


Awesome, right?

Try out the demo code for yourself here 👉🏼 Edit css-variable-theming

The final question is: What is the difference between CSS Variables and pre-processor variables (SCSS, LESS variables, etc.) and which one is better?

CSS Variables vs Pre-processor variables

CSS variables are "native" to browsers, with most modern browsers supported its usage (except Internet Explorer 11 🤷‍♀️). Hence you can work with variables directly in CSS. No pre-compiling is needed. In the Devtool's Element Inspector, the styles with CSS variables are displayed as how you write them.


And overriding their values can be done programmatically by style.setProperty() and the related styles change accordingly, in run-time.

const box = document.getElementById('another-box');

//Change value of CSS variable "--default-color"
box.style.setProperty('--default-color', '#c70085')

Meanwhile, for CSS pre-processors, as indicated in their names, all variables need to be processed and compiled into normal CSS before delivering. Otherwise, the browser won't understand the declaration and ignore them. Take the following SCSS code, for instance:

$defaultColor: #69bac9;

* {
 background-color: $defaultColor;

It will appear on the client-side as

* {
 background-color: #69bac9;

All the SCSS variables don't exist after compiling to CSS, because they are only native to SCSS, not to CSS. Hence, to reuse a variable and assign a different value on run-time is impossible for pre-processor variables.

Besides, since CSS Variables are CSS properties and follow cascade rules, it is possible to leverage them inside of media queries for responsiveness.

:root {
 --font-size-md: 14px;

@media (max-width: 700) {
 :root {
  --font-size-md: 10px;

Unfortunately, it is unable to achieve this behavior without a complicated workaround for CSS pre-processor.

Therefore, why not just use CSS variables by default? No pre-processor, no third-party package needed to install, no worry about version support, and you can do wonders. 😉

Browser support

The browser support for CSS Variables recently is really good. Except for Internet Explorer (which anyway is deprecated), the following are the minimum for CSS variables to work.

For Desktop: Chrome 49, Firefox 31, Edge 16 and Safari 9.1 and above.

For Mobile: Android 81, Firefox 48, Chrome 81 and Safari 9.3


In my opinion, the CSS variable is the big win for CSS in modern web development. The ability to dynamically set a variable and use it consistently throughout all CSS stylings within an application bring ease to developers in organizing CSS codes and speeding up the development process.

Theming is one of the many uses cases where CSS variables shine brightly. However, the advantages of CSS variables over CSS pre-processor variables don’t eliminate the usefulness of CSS pre-processor in general. On the contrary, there are more complex cases that require a combination of both. Hence, as always, choose your tools wisely and think CSS variables first, for a better codebase😉

Made something with CSS variables and want to show it off? Ping me and I will have it added to this post 😉. I always love to see CSS wonders made by others ❤️.

👉 If you'd like to catch up with me sometimes, follow me on Twitter | Facebook.

Like this post or find it useful ? Share it 👇🏼 😉