🎨 Theme Picker

Introduction

When first creating this site in October 2020, introducing themes was always part of the future roadmap. As a result, I've tried my best to consider reusability, scalability, and flexibility when crafting styles, with CSS custom properties at the forefront. Depending on the size and complexity of your frontend ecosystem, including how well styles are centralized for reusable distribution across components, stylesheets, or applications, the tech debt effort to migrate over to custom properties will of course vary.

Backtracking to update everything may seem a little daunting at first. However, if you're seriously considering themes in any personal projects, or perhaps it's come up in a work meeting recently, I'd strongly advise taking some time to audit what you've got. CSS custom properties are your friend and will make theming much simpler.

I'm aware of the approach to set the theme based on the OS preferences, but I couldn't decide on a nice light them. Perhaps a future enhancement. Anyway, enough of this introduction, let's jump straight into CSS custom properties.

CSS Custom Properties

Syntax

We define custom properties using the -- prefix. Another property is set by retrieving the value of a custom property using the var() function. Additional fallback values can be provided as the second argument. To date, I've not needed to set any fallback values.

Note: This isn't a fallback value used by the browser if CSS custom properties aren't supported, just a fallback if the custom property hasn't been created yet.

1$font-fallback: #fff;
2
3:root {
4 --white: #fff;
5 --purple: #e167ff;
6 --lime: #01ff70;
7
8 --primary-color: var(--lime);
9 --secondary-color: var(--purple);
10 --font-color: var(--white, $font-fallback);
11}

Scope

We define our CSS custom properties under the :root selector, which matches the root element of the document tree, enabling access to properties globally. CSS custom properties also cascade, which is the algorithm that defines the combination of property values originating from different sources. (Learn more)

Support

You're good to use CSS custom properties in all modern browsers. However, as expected Internet Explorer has 0 support. There is viable options for providing fallbacks when using CSS custom properties. If you need to support IE you can use the @supports CSS rule. Learn more here.

Dynamic

CSS custom properties are dynamic, which in short means that any changes will be automatically picked up and reflected by the browser. This makes them perfect for updating a site's theme in combination with some JavaScript. đŸ”Ĩ

Using Body Theme Attributes

Here is a quick visual of how the whole cycle works, from a single click through to every themed element on the page updating.

Each click updates the body attribute. The matching CSS selector activates, setting --primary-color to the next colour. Every element using that variable re-paints automatically.

Styles

The styles and sass logic below are responsible for creating our CSS custom properties and various dark theme attribute class selectors. Don't worry, we will walk through each part.

1// CSS Custom Properties
2:root {
3 /* Core Colors */
4 --white: #fff;
5 --orange: #ff9752;
6 --yellow: #faff73;
7 --pink: #fc7eca;
8 --purple: #e581fe;
9 --lime: #6effad;
10 --red: #ff4848;
11 --blue: #7aa2ff;
12
13 --black-navy: #131a25;
14 --navy: #182635;
15
16 --slate-blue: #6a5acd;
17 --dark-slate-blue: #483d8b;
18 --text-slate-blue: #9c8cff;
19 --grey-blue: #4f5966;
20
21 --light-grey: #9ca8b4;
22
23 /* 🎨 Themes 🎨 */
24
25 --background-color-dark-one: var(--black-navy);
26 --background-color-dark-two: var(--navy);
27 --secondary-color: var(--purple);
28 --font-color: var(--white);
29
30 /* Primary color dark themes */
31 $dark-theme-colors: (
32 'text-slate-blue',
33 'lime',
34 'purple',
35 'blue',
36 'yellow',
37 'red',
38 'orange',
39 'pink'
40 );
41
42 // create attribute selector class for each theme
43 @for $i from 1 through length($dark-theme-colors) {
44 $name: nth($dark-theme-colors, $i);
45 body[dark-theme-color='#{$name}'] {
46 --primary-color: var(--#{$name});
47 }
48 }
49
50 $total-dark-themes: length($dark-theme-colors);
51 --dark-theme-colors: #{$dark-theme-colors}; // expose for JS to access
52 --total-dark-themes: #{$total-dark-themes}; // expose for JS to access
53}

First, we define our custom properties.

1// CSS Custom Properties
2:root {
3 /* Core Colors */
4 --white: #fff;
5 --orange: #ff9752;
6 --yellow: #faff73;
7 --pink: #fc7eca;
8 --purple: #e581fe;
9 --lime: #6effad;
10 --red: #ff4848;
11 --blue: #7aa2ff;
12
13 --black-navy: #131a25;
14 --navy: #182635;
15
16 --slate-blue: #6a5acd;
17 --dark-slate-blue: #483d8b;
18 --text-slate-blue: #9c8cff;
19 --grey-blue: #4f5966;
20
21 --light-grey: #9ca8b4;
22
23 /* 🎨 Themes 🎨 */
24
25 --background-color-dark-one: var(--black-navy);
26 --background-color-dark-two: var(--navy);
27 --secondary-color: var(--purple);
28 --font-color: var(--white);
29}

Next, we need to create an attribute class selector for each colour. We do this by using a scss list containing each of our custom property colours, without the custom property -- prefix. We'll add the -- prefix to each colour inside our scss loop using the @for scss directive.

@for definition

counts up or down from one number (the result of the first expression) to another (the result of the second) and evaluates a block for each number in between. Each number along the way is assigned to the given variable name. If to is used, the final number is excluded; if through is used, it's included. (Learn more)

1/* Primary color dark themes */
2 $dark-theme-colors: (
3 'text-slate-blue',
4 'lime',
5 'purple',
6 'blue',
7 'yellow',
8 'red',
9 'orange',
10 'pink'
11 );
12
13 // create attribute selector class for each theme
14 @for $i from 1 through length($dark-theme-colors) {
15 $name: nth($dark-theme-colors, $i);
16 body[dark-theme-color='#{$name}'] {
17 --primary-color: var(--#{$name});
18 }
19 }
20
21 $total-dark-themes: length($dark-theme-colors);
22 --dark-theme-colors: #{$dark-theme-colors}; // expose for JS to access
23 --total-dark-themes: #{$total-dark-themes}; // expose for JS to access

We can see each body element attribute selector class created from our scss loop inside our compiled css file.

1:root body[dark-theme-color="blue"] {
2 --primary-color: var(--blue);
3}
4
5:root body[dark-theme-color="lime"] {
6 --primary-color: var(--lime);
7}
8
9:root body[dark-theme-color="text-slate-blue"] {
10 --primary-color: var(--text-slate-blue);
11}
12
13:root body[dark-theme-color="light-yellow"] {
14 --primary-color: var(--light-yellow);
15}
16
17:root body[dark-theme-color="orange"] {
18 --primary-color: var(--orange);
19}
20
21:root body[dark-theme-color="red"] {
22 --primary-color: var(--red);
23}
24
25:root body[dark-theme-color="pink"] {
26 --primary-color: var(--pink);
27}

We also need to set our default theme value for when our app first loads. This uses the same dark-theme-color attribute on our body element, but this time in our index.html file.

1<body dark-theme-color="text-slate-blue">
2 <div id="root"></div>
3 <script type="module" src="./index.tsx"></script>
4</body>

You could create your styles in a more verbose manner if you prefer the readability, dislike scss, or just need a more sophisticated setup like switching between light and dark themes:

1...
2/* Without a sass loop */
3
4body[dark-theme-color='red'] {
5 --primary-color: var(--red);
6}
7
8body[dark-theme-color='blue'] {
9 --primary-color: var(--blue);
10}
11
12body[dark-theme-color='yellow'] {
13 --primary-color: var(--yellow);
14}
15
16/* Example light theme */
17body[theme-type='light'] {
18 --background-color-dark-one: white;
19 --background-color-dark-two: var(--light-grey);
20 --font-color: var(--black-navy);
21}
22
23...

JavaScript

Now that we've created all the indexed theme CSS custom properties, we need some JavaScript to interact with the Window.getComputedStyle() method. This method provides us with an interface to retrieve and update our CSS custom properties. We can use the getPropertyValue() interface method to obtain a property value and the setProperty() interface method to update a property using the property name and the new value we want to update it with. We won't be using the setProperty() interface method, instead we will be updating the dark-theme-color attribute on the body element.

1const getProperty = (
2 name: string,
3 computedStyleInterface: Record<string, any>
4): string => computedStyleInterface.getPropertyValue(name).trim();
5
6const computedStyleInterface: Record<string, any> = getComputedStyle(
7 document.documentElement
8);
9
10// get custom properties
11getProperty('--total-dark-themes', computedStyleInterface)
12getProperty('--dark-theme-colors', computedStyleInterface).split(',')

You can copy & paste the snippet above (without types) into the browser and see the output as: (7) ['blue', ' lime', ' text-slate-blue', ' light-yellow', ' orange', ' red', ' pink']

Before we look at the full component snippet. The gist of it is, the user clicks the change theme button and we add the next theme color as the value on our body elements dark-theme-color attribute.

1const changeTheme = (index: number) => {
2 const nextThemeColor = themeColors[index].trim();
3 body?.setAttribute('dark-theme-color', nextThemeColor);
4};

Our component...

1import React, { FC, useState, useCallback, useLayoutEffect } from 'react';
2import ThemeSvg from './ThemeSvg';
3
4const getProperty = (
5 name: string,
6 computedStyleInterface: Record<string, any>
7): string => computedStyleInterface.getPropertyValue(name).trim();
8
9interface Props {
10 className: string;
11 tabbable?: boolean;
12}
13
14const changeTheme = (index: number, themeColors: string[]): void => {
15 const nextThemeColor = themeColors[index].trim();
16 const body = document.querySelector('body');
17 body?.setAttribute('dark-theme-color', nextThemeColor);
18};
19
20const accessibilityText = 'change dark theme primary color';
21
22const ThemePicker: FC<Props> = ({ className, tabbable }) => {
23 const [currentIndex, setCurrentIndex] = useState<number>(0);
24 const [totalThemes, setTotalThemes] = useState<number>(0);
25 const [themeColors, setThemeColors] = useState<string[]>([]);
26
27 useLayoutEffect(() => {
28 const computedStyleInterface: Record<string, any> = getComputedStyle(
29 document.documentElement
30 );
31
32 setTotalThemes(
33 Number(getProperty('--total-dark-themes', computedStyleInterface))
34 );
35 setThemeColors(
36 getProperty('--dark-theme-colors', computedStyleInterface).split(',')
37 );
38 }, []);
39
40 const clickHandler = useCallback((): void => {
41 let nextIndex = currentIndex + 1;
42
43 const isLastTheme: boolean = nextIndex === totalThemes;
44 if (isLastTheme) nextIndex = 0;
45
46 changeTheme(nextIndex, themeColors);
47 setCurrentIndex(nextIndex);
48 }, [changeTheme, setCurrentIndex, currentIndex, themeColors, totalThemes]);
49
50 if (totalThemes === 0 || themeColors.length === 0) {
51 <button className={className} />;
52 }
53
54 return (
55 <button
56 className={className}
57 aria-label={accessibilityText}
58 title={accessibilityText}
59 onClick={clickHandler}
60 tabIndex={tabbable ? 0 : -1}
61 >
62 <ThemeSvg className={`${className}--svg`} />
63 </button>
64 );
65};
66
67export default ThemePicker;

We use useLayoutEffect to read layout from the DOM, update our local state hook values with our total dark themes/dark theme colors, and then synchronously re-render.

When you change theme with Chrome dev tools open, you'll notice the new attribute class being added to the body element each time the color changes. For example, changing from pink to lime before and after:

1// before
2:root body[dark-theme-color=pink] {
3 --primary-color: var(--pink);
4}
5
6// after
7:root body[dark-theme-color=lime] {
8 --primary-color: var(--lime);
9}

Here's another fun example using a native color picker input to change the CSS custom property used as the background color for this div element.

1const styleInterface = document.documentElement.style;
2
3<label>
4 Background Color:
5 <input
6 type="color"
7 onChange={(event) =>
8 styleInterface.setProperty(`--example`, event.target.value)
9 }
10 />
11</label>;
12
13<div style={styles.example} />
14
15// css module
16.example {
17 background-color: var(--example);
18 height: 100px;
19 width: 100%;
20 border-radius: 32px;
21 margin: 16px;
22}

Final Thoughts

That's it for this article, go forth and make some cool themes with CSS custom properties! 🚀 Oh and make sure to try changing the theme / inverting the background colors in the settings toolbar (toggle the settings cog to view!).

Was this useful?

Thanks for reading! Time for one more?

const xmas = christmasChallenge()
// 5 puzzles to unlock 🎅
xmas.santaSecretPassPhrase('???')
xmas.giveSantaFood('???')

🎅 Christmas Challenge

16 Dec 2021
👨‍đŸ’ģ Coding challenge

An interactive christmas challenge. Use your web dev skills to help Santa at the northpole 🎅.

0
0
0
JAVASCRIPT
const urls = routes.map(r => `${baseUrl}${r}`)
await Promise.all(
urls.map(url => $`npx axe ${url}`)
)

đŸĒ“ Google ZX & Axe CLI

16 Oct 2021 â€ĸ 📖 4 min read â€ĸ Updated 15 Mar 2026
⚡ Lightning read

Running axe-core CLI with Google ZX.

0
0
0
ACCESSIBILITY NODE