Dark mode button with MaterializeCSS 2
This article walks through implementing a dark mode button with OS-level theme detection using MaterializeCSS 2.x. MaterializeCSS has been progressing towards support for Material Design M3 since version 2.
We are going to create a component like this. The center button is for detecting and following the OS’s theme setting.
MaterializeCSS’s Theme documentation covers creating a light/dark toggle button, but what about detecting and following the OS’s theme setting? Here’s how we can make that work.
Core Implementation
Thanks to the good design of MaterializeCSS, the core code is surprisingly simple. This snippet adds a theme="light" or theme="dark" attribute to the <html> tag, and if the user selects OS mode, it removes the attribute altogether:
if (mode === 'dark' || mode === 'light') {
document.documentElement.setAttribute('theme', mode);
} else if (mode === 'os') {
document.documentElement.removeAttribute('theme');
}
Why This Code Works?
:root {
color-scheme: light;
--md-sys-color-surface: var(--md-sys-color-surface-light);
--md-sys-color-on-surface: var(--md-sys-color-on-surface-light);
}
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
--md-sys-color-surface: var(--md-sys-color-surface-dark);
--md-sys-color-on-surface: var(--md-sys-color-on-surface-dark);
}
}
:root[theme='light'] {
color-scheme: light;
--md-sys-color-surface: var(--md-sys-color-surface-light);
--md-sys-color-on-surface: var(--md-sys-color-on-surface-light);
}
:root[theme='dark'] {
color-scheme: dark;
--md-sys-color-surface: var(--md-sys-color-surface-dark);
--md-sys-color-on-surface: var(--md-sys-color-on-surface-dark);
}
:root {
--md-sys-color-surface-light: #fcfcff;
--md-sys-color-on-surface-light: #1a1c1e;
--md-sys-color-surface-dark: #1a1c1e;
--md-sys-color-on-surface-dark: #e2e2e5;
}
Adding color-scheme to CSS
:root {
color-scheme: light dark;
}
This tells the browser to automatically apply the appropriate color scheme based on user settings or OS preferences.
How the Themes Apply in Different Scenarios
Here’s how this setup behaves in different situations:
- If the <html> tag has theme="light", the CSS variables in :root[theme='light'] are used.
- If theme="dark" is set, the CSS variables in :root[theme='dark'] are applied.
- If there’s no theme attribute and the OS is in dark mode, the CSS under @media (prefers-color-scheme: dark) takes effect.
- If the OS is in light mode and there’s no theme attribute, the default :root CSS applies.
Full Component Example
Here’s the complete code of our <dark-mode> button component. It uses a nice JavaScript framework, Aurelia (1)and Aurelia-Materialize bridge, but you can adapt the idea for any JavaScript setup. The selected mode is saved in localStorage, allowing you to restore it with restoreDarkMode() when the app starts. '.active-shade' class works both in light mode and dark mode.
- app.html <style> :root { color-scheme: light dark; } </style> <require from="../resources/dark-mode"></require> <dark-mode></dark-mode>
- dark-mode.html <template> <style> .active-shade .active { background-color: rgba(128, 128, 128, 0.2); } </style> <div class="center active-shade"> <button md-button="flat: true;" click.delegate="setDarkMode('light')" class="${mode=='light' ? 'active' : ''}" title="${'dark.light' & t}"> <i class="material-icons-outlined">light_mode</i> </button> <button md-button="flat: true;" click.delegate="setDarkMode('os')" class="${mode=='os' ? 'active' : ''}" title="${'dark.os' & t}"> <i class="material-icons-outlined">computer</i> </button> <button md-button="flat: true;" click.delegate="setDarkMode('dark')" class="${mode=='dark' ? 'active' : ''}" title="${'dark.dark' & t}"> <i class="material-icons-outlined">dark_mode</i> </button> </div> </template>
- dark-mode.ts type DarkModeType = 'light'|'os'|'dark'; /** Manages dark mode. */ export class DarkMode { mode: DarkModeType; constructor() { this.mode = getDarkMode(); } /** Sets light/dark mode. @params mode Any of DarkModeType. If mode is not right, 'light' is used. */ setDarkMode(mode: DarkModeType) { this.mode = setDarkMode(mode); } } /** Gets light/dark mode. */ export function getDarkMode(): DarkModeType { return localStorage.darkMode || 'light'; } /** Sets light/dark mode. @params mode Any of DarkModeType. If mode is not right, 'light' is used. @returns A canonical mode. */ export function setDarkMode(mode: DarkModeType): DarkModeType { if (!['light', 'os', 'dark'].includes(mode)) mode = 'light'; if (mode === 'dark' || mode === 'light') { document.documentElement.setAttribute('theme', mode); } else if (mode === 'os') { document.documentElement.removeAttribute('theme'); } localStorage.darkMode = mode; return mode; } /** Restores light/dark mode from localStorage. */ export function restoreDarkMode() { setDarkMode(getDarkMode()); }
If we need to switch colors of a class between light mode and dark mode, a set of classes like this is required.
/* for explicit light mode */ .shaded { background-color: rgba(128, 128, 128, 0.15) !important; } /* for explicit dark mode */ html[theme="dark"] .shaded { background-color: rgba(128, 128, 128, 0.35) !important; } /* for OS dark mode and not explicit light mode */ @media (prefers-color-scheme: dark) { html:not(html[theme="light"]) .shaded, background-color: rgba(128, 128, 128, 0.35) !important; } }