MaterializeCSS 2で作るダークモード・ボタン
MaterializeCSS 2.xを使用して、OSレベルのテーマ検出を行うダークモードボタンの実装方法を説明します。MaterializeCSSはバージョン2からMaterial Design M3への対応が進んでいます。
このようなボタンのコンポーネントを作成します。中央のボタンは、OSのテーマ設定に従うためのものです。
MaterializeCSSのテーマ設定についてのドキュメントでは、ライト/ダークのトグルボタンの作成について説明していますが、OSのテーマ設定を検出して従う方法については触れられていません。ここでは、その実現方法を紹介します。
実装の核心
MaterializeCSSがよくできているお陰で、基本的なコードは驚くほどシンプルです。このコードは、<html>
タグにtheme="light"
またはtheme="dark"
属性を追加するか、ユーザーがOSモードを選択した場合は属性を削除します:
if (mode === 'dark' || mode === 'light') { document.documentElement.setAttribute('theme', mode);
} else if (mode === 'os') {
document.documentElement.removeAttribute('theme');
}
なぜこのコードは機能するのか?
このコードが機能する理由は、CSS変数とメディアクエリ(@media)が自動的に選択されたテーマに適応するためです。以下は、ライトモードとダークモードのCSS変数を設定するtheme.module.scss
の抜粋です:
: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);
}
変数--md-sys-color-surface
は背景の色を表し、ライトモードでは白(#fcfcff
)、ダークモードでは黒(#1a1c1e
)になります。theme
属性を切り替えることで、ブラウザはテーマに応じた色を自動的に選択します。
CSSにcolor-scheme
を追加
両方のモード(ライトとダーク)を完全にサポートするために、CSSにcolor-scheme: light dark;
を追加する必要があります:
:root { color-scheme: light dark;
}
これにより、ブラウザはユーザーやOSの設定に応じて、適切な色のスキームを自動的に適用します。
CSSはどう適用されるか
このような設定は以下のように機能します。
<html>
タグにtheme="light"
が設定されている場合、:root[theme='light']
のCSS変数が適用されます。theme="dark"
が設定されている場合、:root[theme='dark']
のCSS変数が適用されます。- 属性
theme
がなく、OSがダークモードの場合は、@media (prefers-color-scheme: dark)
のCSS変数が適用されます。 - 属性
theme
がなく、OSがライトモードの場合は、デフォルトの:root
のCSS変数が適用されます。
コード全体
以下が<dark-mode>コンポーネント全体のコードです。Aurelia (1)という、とても簡単に書けるフレームワークとAurelia-Materialize Bridgeを使用しています。方式自体は他のJavaScriptフレームワークでも適用可能です。
選択したモードはlocalStorage
に保存され、アプリケーションの起動時にrestoreDarkMode()
を呼び出すことで復元できます。.active-shadeはライトモードでもダークモードでもボタンに選択状態を表す影をつけます。
- 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()); }
/* 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; } }