Theme switch
A theme switch button component for manually switching color-schemes whilst respecting user preferences for light or dark mode viewing, also includes options for styling the button with Themalize icons plus two optional pre-styled buttons.
Requirements (anchor)
With $enable-manual-themes: true;
in the configuration.scss
document the @media
query used for the dark mode color-schemes is changed to use a data-attribute
style.
Example
@media (prefers-color-scheme: dark) {
:where(html) {
color-scheme: dark;
/* dark mode variables */
}
}
:where([data-prefers-dark=true]) {
color-scheme: dark;
/* dark mode variables */
}
The script theme.js
[assets/js]
needs to be loaded prior to the style sheets in the head of the HTML document (to negate FOUC when changing or refreshing pages).
Script
The script has been adapted from Adam Argyle's Building a theme switch component article on Google's web.dev to use the aria-pressed
attribute to indicate button status following advice provided in an Adrian Roselli article on improving the accessibility of the same button and script as it was being used on a Google promotional website.
const storageKey = 'theme-preference'
const onClick = () => {
theme.value = theme.value === 'false'
? 'true'
: 'false'
setPreference()
}
const getColorPreference = () => {
if (localStorage.getItem(storageKey))
return localStorage.getItem(storageKey)
else
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'true'
: 'false'
}
const setPreference = () => {
localStorage.setItem(storageKey, theme.value)
reflectPreference()
}
const reflectPreference = () => {
document.firstElementChild
.setAttribute('data-prefers-dark', theme.value)
document
.querySelector('#themes')
?.setAttribute('aria-pressed', theme.value)
}
const theme = {
value: getColorPreference(),
}
reflectPreference()
window.onload = () => {
reflectPreference()
document
.querySelector('#themes')
.addEventListener('click', onClick)
}
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', ({matches:isDark}) => {
theme.value = isDark ? 'true' : 'false'
setPreference()
})
A button with the same attributes needs to be included somewhere on the page. It's provided unstyled so can be designed using your own CSS or with the styling options described below.
<button id="themes" aria-pressed="false">Dark mode</button>
Theme switch button styling (anchor)
Designed for the pre-styled button options, or to work with custom button styles using the Themalize icons, the following method can also used with your own theme switch button styles if preferred.
With $enable-theme-switch: true;
the following variables are compiled with the color-scheme variables in the _variables.scss
document, the attributes are designed to work with the pre-styled buttons as described below.
:where(html) {
color-scheme: light;
/* light mode variables */
--switch-ico: var(--switch-off);
}
:where([data-prefers-dark=true]) {
color-scheme: dark;
/* dark mode variables */
--switch-ico: var(--switch-on);
}
Applied through the color-schemes the icons don't suffer from FOUC when changing or refreshing pages, so it's recommended if designing your own theme switch button to apply the styles using the same method.
Pre-styled buttons (anchor)
Two optional theme switch buttons use the variables above with the Themalize icons and have been written to work independently if the individual icons or utility styles are not enabled in the configuration.scss
document.
$enable-switch-celestial: true;
$enable-switch-radio: true;
Dark offDark on
Both use the same attributes so the button doesn't have to be altered when changing styles. The .vis-hidden
text utility is included with the typography styles by default.
<button id="themes" aria-pressed="false"><span class="vis-hidden">Dark mode</span></button>
The styles are provided in the _theme-switch.scss
document in the [styles/components]
directory.
_theme-switch.scss
Currently the styles still require $enable-forms-buttons: true;
for the basic button styles but the icon styling will still work with fallback user-agent styles so can be customized as required if the button styles are disabled.
@if $enable-manual-themes and $enable-theme-switch {
// Celestial button
@if $enable-switch-celestial {
@if $enable-sun-svg and $enable-moon-svg {
:where(html) {
--switch-off: var(--sun);
--switch-on: var(--moon);
}
}
@else {
:where(html) {
--switch-off: #{$sun};
--switch-on: #{$moon};
}
}
#themes {
--ico: var(--color);
--svg: var(--switch-ico);
--btn-px: .5rem;
--ico-va: -.12em;
}
#themes:before {
@extend %icon-mask;
}
#themes:after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
}
:where(html) {
#themes:after {
content: "Dark off" / "off";
}
}
:where([data-prefers-dark=true]) {
#themes:after {
content: "Dark on" / "on";
}
}
} // END [if/celestial]
// Radio button
@if $enable-switch-radio {
@if $enable-radio-off-svg and $enable-radio-on-svg {
:where(html) {
--switch-off: var(--radio-off);
--switch-on: var(--radio-on);
}
}
@else {
:where(html) {
--switch-off: #{$radio-off};
--switch-on: #{$radio-on};
}
}
#themes {
--btn-bg: transparent;
--btn-hover: transparent;
--btn-bd-color: transparent;
--btn-py: 0;
--btn-px: 0;
--ico: var(--color);
--svg: var(--switch-ico);
--ico-va: -.12em;
}
#themes:before {
@extend %icon-mask;
}
#themes:hover {
--color: var(--link-hover);
}
#themes:hover:after {
color: var(--text);
}
#themes:after {
margin-inline-start: .075rem;
word-spacing: -.16em;
}
:where(html) {
#themes:after {
content: "Dark off"; // Firefox fallback
content: "Dark off" / "off";
}
}
:where([data-prefers-dark=true]) {
#themes:after {
content: "Dark on"; // Firefox fallback
content: "Dark on" / "on";
word-spacing: -.1em;
}
}
} // END [if/radio]
} // END [if/theme-switch]
Both buttons are designed to be accessible using the aria-pressed
attribute to indicate status as described above combined with visually hidden text to describe the button. The CSS content
attributes used on both pre-styled buttons is designed to enhance accessibility but please note currently hasn't been tested beyond the author's devices so email feedback is highly welcomed.