Dark mode button with MaterializeCSS 2

25 oct 2024   HIRANO Satoshi

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.

dev CSS Material Design Materialize Aurelia
Síganos
Compartir

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?

The reason this snippet is effective comes down to CSS variables and media queries, which automatically adapt to the chosen theme. Below is an excerpt from MaterializeCSS's theme.module.scss where CSS variables are set up for light and dark modes:

     :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;
    }

The --md-sys-color-surface variable represents the background color, which is white (#fcfcff) in light mode and black (#1a1c1e) in dark mode. By switching the theme attribute, the browser picks up the correct colors according to the theme.

Adding color-scheme to CSS


To fully support both light and dark modes, you’ll need to add color-scheme: light dark; to your 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;
      }
  }