Theme switcher
The Theme Switcher lets users toggle between light, dark, and system themes, with the latter adapting to the device's preference. It's powered by two JavaScript scripts—one in the head section and one at the bottom of the page—to ensure smooth transitions. Several variants cater to different UI needs.
Default
<fieldset>
<legend>Theme</legend>
<div class="bg">
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="themeOptions" data-bs-theme-value="auto" aria-label="System" id="theme-system">
<label for="theme-system" class="form-check-label">System</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="themeOptions" data-bs-theme-value="light" aria-label="Light" id="theme-light">
<label for="theme-light" class="form-check-label">Light</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="themeOptions" data-bs-theme-value="dark" aria-label="Dark" id="theme-dark">
<label for="theme-dark" class="form-check-label">Dark</label>
</div>
</div>
</fieldset>
It is advisable to add id
attributes to radio buttons and corresponding for
attributes to labels. This enhancement allows users to click the label as well as the radio button itself, improving accessibility and usability. However, the code functions independently of these id attributes, permitting the addition of multiple switchers to the page as needed.
Hamburger menu
This variant is designed to sit on a blue background and can be used in a site's context or hamburger menu. It merely changes the colour of the title to white
<form class="theme-switch hamburger-menu">
...
</form>
Compact
In this variant, all the options presented slightly smaller and inline. A thin divider is present at the top of the switcher
<form class="theme-switch theme-compact">
...
</form>
Right nav
When using the theme switcher underneath the right hand nav, simply add the class theme-right-nav
to get the appropriate amount of right margin. You can see this in action underneath the nav panel.
<form class="theme-switch theme-compact theme-right-nav">
...
</form>
Javascript
The first script is placed in the <head>
to ensure the theme is set as early as possible,
reducing the flash of unstyled content (FOUC). It determines the user's preferred
theme (either from local storage or system settings) and applies it immediately.
<script>
(function() {
'use strict';
const getStoredTheme = () => localStorage.getItem('theme');
const getPreferredTheme = () => {
const storedTheme = getStoredTheme();
return storedTheme ? storedTheme : (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
};
const setTheme = theme => {
const themeToSet = theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme;
document.documentElement.setAttribute('data-bs-theme', themeToSet);
};
setTheme(getPreferredTheme());
})();
</script>
The second script, placed at the bottom of the page, handles the theme switching functionality for the website. It retrieves the user's preferred theme from local storage or system settings and applies it to the document. It also provides the ability to switch themes through a theme switcher UI component, updates the UI to reflect the active theme, and listens for changes to the system's dark mode preference.
This script uses data-bs-theme-value
to drive the logic, rather than ids, to allow multiple instances of the switcher to be present on a single page. This is unlikely, but may occur, a switcher in the hamburger menu and a compact version at the bottom of the page.
<script>
(function() {
'use strict';
const getStoredTheme = () => localStorage.getItem('theme');
const setStoredTheme = theme => localStorage.setItem('theme', theme);
const getPreferredTheme = () => {
const storedTheme = getStoredTheme();
return storedTheme ? storedTheme : (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
};
const setTheme = theme => {
const themeToSet = theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme;
document.documentElement.setAttribute('data-bs-theme', themeToSet);
};
const showActiveTheme = theme => {
document.querySelectorAll('.theme-switch').forEach(themeSwitcher => {
themeSwitcher.querySelectorAll('[data-bs-theme-value]').forEach(element => {
element.checked = (element.getAttribute('data-bs-theme-value') === theme);
});
});
};
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
const storedTheme = getStoredTheme();
if (storedTheme !== 'light' && storedTheme !== 'dark') {
setTheme(getPreferredTheme());
}
});
window.addEventListener('DOMContentLoaded', () => {
showActiveTheme(getPreferredTheme());
document.querySelectorAll('.theme-switch [data-bs-theme-value]').forEach(toggle => {
toggle.addEventListener('change', () => {
const theme = toggle.getAttribute('data-bs-theme-value');
setStoredTheme(theme);
setTheme(theme);
showActiveTheme(theme);
});
});
});
})();
</script>