Page Theme Switcher
Overview
This is how I'm doing my color mode/theme/scheme switcher:
- Create individual CSS Custom Property/Variables for each theme. I'm using three ('light', 'dark', and 'high-contrast'), but you can make as many as you'd like.
- Create light and dark mode variables that point to the individual variables for each theme. (Nothing is done with 'high-contrast' at this stage as it's only set by JavaScript).
- Create element styles that call `*--switch` variables that don't initially exist with fallbacks to the mode specific variables.
- Add a script tag to the head of the document that uses JavaScript (if it's available) to populate the `*--switch` variables based of user selections on the page.
The result is a progressively enhanced theme setup. JavaScript is used to set the theme if everything is functional. Values fall back to the `prefers-color-scheme` if JavaScript is either unavailable or if no manual preference has been set. If the `prefers-color-scheme` value isn't found the styles fall back to the non-customized version.
Live Example
Details
The CSS
My style sheet breaks down into four sections:
-
The base color variables for each theme
} -
Default variables set to use the primary `--light-*`` variables
} -
Dark mode variables set to use the primary `--dark-*`` variables
{ } } -
Page element styles that set to use `*--switch` variables (that don't exist yet) with fallbacks to the basic variable names.
} }
The JavaScript
This script tag goes directly in the <head> of the page directly after the call for the stylesheet.
The first thing the script does is make the updateStyles()
function. The styleKeys array inside it
defines the list of styles to update based off the
naming the naming convention that maps the base
colors to the *--switch versions.
The function checks to make sure a theme
value is passed then uses it to determine what
happens next.
It it's auto,
the function removes the custom *--switch properties.
Otherwise, it adds them with the given theme name.
After the properties, the function also updates the localStorage value with the theme. Doing that means all the storage checking and setting happens in the same place. No need to mess with it in whatever scripting is used for the interface.
After the function is set up, the script makes an initial call to it with the current localStorage value for `theme`.
The Interface
The last step is making an interface on the page.
I'm using bitty.
Other approaches would work just as well. The
only real requirement is calling the
updateStyles() function with
the name of the theme.
bitty approach
Since the theme switcher doesn't work without JavaScript. So, I use this empty HTML element that gets populated directly by it. That way, the HTML radio buttons don't show up on the page if the JavaScript fails for some reason.
And then this in the `/scripts/theme-switcher.js` file:
const themes = ;
const tmpl = `<div><label>
<input type="radio"
name="mode-LOCATION"
value="KEY"
data-send="changeTheme"
data-receive="syncChecked"
CHECKED
/>NAME</label></div>`;
;
Full Example
Here's a full HTML page to show how everything looks when combined that you can check out here.
Theme Switcher Example
Home page
Theme Switcher Example
The `/styles/example-styles.css` file looks like this:
}
}
{
}
}
}
}
Endnotes
- Some folks have Strong Opinions about putting inline script tags on pages. I like the approach here because it helps ensure there's no flash between colors when a color is found/set on the JavaScript side.
-
I'm intentionally avoiding a
noscripttag for the color theme switcher. It's not critical for the content or for the page to function. - This approach requires explicitly listing all the keys for the styles you want to change (e.g. `background`, `color`, `link`). Another approach would be to loop through all the properties and do a find/replace style update. That ends up being a lot more code. I may switch to that at some point, but I don't have that many updates to my site where I need to adjust what styles are getting updated.
- I'm not doing anything with `color-scheme: light dark;` in this example. I still need to dig into it to figure out how to make it reactive to JavaScript when that's what sets the theme. (e.g. switching to the `light` version when JavaScript sets the theme to `light`, but the value coming in from `prefers-color-scheme` is `dark`.
- I'm not doing anything to update different windows if you have multiple pages open when you change themes. That's entirely possible, but not worth the complexity for the sites I generally build.
- The bitty code includes a `load-hider` that sets the opacity of the main element to zero until it's finished loading. That's not strictly necessary for the theme switcher. However, it does prevent the page from jumping around visually as the theme switcher buttons are populated. (noscript and JavaScript setTimeout fallbacks are included to cover times when the main script doesn't load or JavaScript isn't available.)
- The interface I'm building on these pages with bitty provides the ability to have multiple instances of the switcher on the page and keep them all in sync. That's not necessary, but I like having the feature.
- bitty is a web component that makes pages interactive. You can go here to learn more about it.