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.
Top comments (0)