色モードの切り替え

Windows 10では「アプリ モード(App mode)」、Android 10では「ダークモード(Dark Theme)」、macOS 10.15やiOS 13では「外観モード(Appearance)」と、それぞれ呼ばれている色モードを尊重しつつ、ユーザーの好みで明るくも暗くもできるようにした。どう実装すると効率的かを考えたかっただけなので、次期マイナーバージョンでは消える。

ウェブページでの色モードの設定には、3つの状態の管理が必要らしい。ダーク・モードにしているが特定のウェブページは白背景で見たい、またはその逆があるため、自動(OSやブラウザーでユーザーが設定したモードに従う)と、強制的にダーク、そして強制的にライトにできるべきという主張だ。実装はあまり見ないが、主張は散見される。

僕はアプリケーション側、つまりブラウザーが本来持っているべき設定だと思うので、あまり納得はしていないが、ないものはないので実装するしかなさそうだ。代替スタイルシートの切り替えなどと同じく、ブラウザーに実装されないかもしれないので、なおさらだろう。

:root {
	--color-primary: var(--palette-accent-dark);
	--color-background: var(--palette-lightest);
	--color-surface: var(--palette-light);
	--color-on-background: var(--palette-darkest);
}

@media (prefers-color-scheme: dark) {
	:root {
		--color-primary: var(--palette-accent-light);
		--color-background: var(--palette-darkest);
		--color-surface: var(--palette-dark);
		--color-on-background: var(--palette-lightest);
	}
}

:root.js-color-mode-light {
	--color-primary: var(--palette-accent-dark);
	--color-background: var(--palette-lightest);
	--color-surface: var(--palette-light);
	--color-on-background: var(--palette-darkest);
}

:root.js-color-mode-dark {
	--color-primary: var(--palette-accent-light);
	--color-background: var(--palette-darkest);
	--color-surface: var(--palette-dark);
	--color-on-background: var(--palette-lightest);
}

色の反転はカスタム・プロパティーで、ユーザーによる強制はクラス名で、それぞれCSSで管理する。いろいろなウェブサイトで解説されているように、パレットを定義しておいて参照するだけにしておくと簡単だ。最初の2つのルールセットが自動の時に使われるもので、あとの2つがユーザーが設定した時に使われる。詳細度がうまく機能するので、これらルールセットはほぼ順不同だが、この順序が書きやすいだろう。

const changeColorMode = (mode) => {
	localStorage.setItem("color-mode", mode);
	const rootClass = document.documentElement.classList;

	if (mode === "dark") {
		rootClass.add("js-color-mode-dark");
		rootClass.remove("js-color-mode-light");
		return;
	}

	if (mode === "light") {
		rootClass.add("js-color-mode-light");
		rootClass.remove("js-color-mode-dark");
		return;
	}

	rootClass.remove("js-color-mode-dark", "js-color-mode-light");
	localStorage.removeItem("color-mode");
};

ユーザー設定の保存はローカル・ストレージにした。「自動」に戻すと削除する。復元する時はhtml要素にクラス名を振っている。そのまま保存できるカスタム・データ属性の方が書きやすいが、スタイル目的なのでクラス名を振るのが正しいと思う。ややこしいことは、ややこしいことができる仕組みの中で、できるだけやるべきだ。

また、このコードはページ読み込み中に1回呼ばれ、状態が復元される。これを非同期で実行すると、画面がフラッシュするかもしれないので、同期で実行すべきだろう。スクリプト・ファイルを分割するとよいだろう。

const onchange = (event) => {
	changeColorMode(event.srcElement.value);
};

const elements = document.querySelectorAll(".js-color-mode");

for (const element of elements) {
	element.addEventListener("change", onchange);
}

あとはラジオ・ボタンの切り替えで色モードを変えるコードを呼ぶだけだ。value属性をよしなにしておけば数行で片付く。


クラス名については:has()があれば必要なくなり、ローカル・ストレージへの保存と復元だけで済みそうなので、早く欲しい。たぶん:root:has(#color-mode-light:checked)と書けるようになる(#color-mode-lightは、ペアになるlabel要素用にあるであろうid属性の値)。