DEV Community

loading...

Easy CSS theming with variables

grumpytechdude profile image Alex Sinclair ・4 min read

I recently had to tackle theming for two projects, and here is what I learned,

CSS Variables are fantastic

--colour-blue = #0074D9;
--colour-blue-dark = #001F3F;
--colour-blue-light = #7FDBFF;

What we've done here is define what our colour blue looks like. So any time we need a blue, we've got standard ones!

"But wait," I hear you say, "that's just a standard colour palette! What's that got to do with theming?"

CSS Variables can reference other variables

--colour-primary = var(--colour-blue);
--colour-primary-active = var(--colour-blue-light);
--colour-primary-disabled = var(--colour-blue-dark);

What we've done here is split the palette colours from the design colours. (If anybody knows the proper names of these, please educate me!)

This means we are in a place to swap out our variables, and CSS will just... Work.

Applying the theme

First things first! CSS Variables are scoped to whatever class they're in. I want to apply it to the whole dom, because I'm just declaring variables - but you can assign it to whatever your top element is.

:root {
  --colour-blue = #0074D9;
  --colour-blue-dark = #001F3F;
  --colour-blue-light = #7FDBFF;

  --colour-orange = #FF851B;
  --colour-orange-dark = #FF4136;
  --colour-orange-light = #FFDC00;

  --colour-primary = var(--colour-blue);
  --colour-primary-active = var(--colour-blue-light);
  --colour-primary-disabled = var(--colour-blue-dark);
}

By putting the primary colours there, we've defined a default theme. This means if no other theme is applied, this will take effect. You could also do what I do, which is not define a default theme. That makes it more obvious while developing if you've missed something, I think.

Next, we define our themes as simple classes

.theme-light {
  --colour-primary = var(--colour-blue);
  --colour-primary-active = var(--colour-blue-light);
  --colour-primary-disabled = var(--colour-blue-dark);
}

.theme-dark {
  --colour-primary = var(--colour-orange);
  --colour-primary-active = var(--colour-orange-light);
  --colour-primary-disabled = var(--colour-orange-dark);
}

Now, on our root element we can define the default theme

<div class='theme-light'></div>

Previewing Themes

Turns out if you apply a theme to an element, that theme takes precedence over the top-level theme. So if you wanted to display a theme switching button in the theme it will switch to, it's as simple as

<div class='theme-light>
  <button>Some Button</button>
  <button class='theme-dark'>Dark Theme</button>
</div>

Switching Themes

To switch themes is to just replace each usage of one theme with another. This is fairly simple to do in JS - you can either keep a list of theme components and query them directly, or you can use the newer document.querySelectorAll() method.

function toggleTheme() {
  const lightElements = document.querySelectorAll('.theme-light');
  const darkElements = document.querySelectorAll('.theme-dark');

lightElements.forEach(element => {
  element.classList.remove('theme-light');
  element.classList.add('theme-dark');
})
darkElements.forEach(element => {
  element.classList.remove('theme-dark');
  element.classList.add('theme-light');
})

And to trigger it:
<button onclick='toggleTheme()'>Toggle Theme</button>

Remembering User Theme Preference

This is a slight tricky one, if you're doing client side rendering. It's probably tricky if you're doing SSR too - but I don't know how you'd do that. Cookies or custom http headers maybe? Anyway!

To store the theme

I used local storage, but a cookie would work too. To store what theme the user has selected, in the toggle theme function you can add this line:

localStorage.setItem('theme', 'theme-dark')

To apply the theme

If you wait for the page to be fully loaded before applying the users' theme, you'll get a flash of the initial theme - and that's not good! I don't think it's perfect, but I put a cheeky script tag directly below my theme element in my html.

<div id='theme-element' class='theme-light'>
<script>const el = document.getElementById('theme-element')
el.classList.remove('theme-light')
el.classList.add(localStorage.getItem('theme') || 'theme-light')
</script>

I bet if you're using React or similar, you can probably just do it in the component.
<div className={localStorage.getItem('theme') || 'theme-light}>

Use Variables by area, not result

In the first project I themed, I'd already written the CSS beforehand, and had to retrofit the theming. This lead to a problem - areas that shared a colour in one theme, did not in another. This meant I couldn't just replace blue with orange! In the end I wound up naming the areas, and setting them in the theme.

For example, I'd used

.header {
  background-color: var(--color-blue-dark);
}
.dropdown-list {
  background-color: var(--color-blue-dark);
}

The problem here is that now we wanted the header to be grey, and the drop-down list to be orange!
In the end I wound up with

.header {
  background-color: var(--header-colour-background);
}
.dropdown-list {
  background-color: var(--dropdown-colour-background);
}

.theme-light {
  --header-colour-background = var(--color-blue-dark);
  --dropdown-colour-background = var(--color-blue-dark);
}

.theme-dark {
  --header-colour-background = var(--color-grey-light);
  --dropdown-colour-background = var(--color-orange);
}

Is there better ways of achieving this? I bet there are! If you've got any input, I'd love to hear it.

Discussion

pic
Editor guide