7月5日、Ollie Williamsが「Dark mode with web standards」と題した記事を公開した。外部ライブラリを一切使わず、Web標準だけでダークモードを完結させる実装を詳細に解説した内容だ。
CSSの light-dark() 関数のグラデーション対応や、JavaScriptから prefers-color-scheme を上書きするUser Preferences APIの議論など、Web標準のダークモード周辺仕様はここ1〜2年で急速に進化している。その一方で「OSの設定に追従しつつ、ユーザーがサイト単位でも切り替えられる」という基本要件を、ライブラリなしで正しく実装しようとすると、思わぬ落とし穴にはまりやすい。本記事はその落とし穴を体系的に整理し、現時点でどこまでWeb標準で実現できるかを明確にしている。
メタタグを起点にした設計
実装の基本となるのが <meta name="color-scheme"> タグだ。CSSの color-scheme プロパティと役割は似ているが、CSSファイルの読み込み完了前に適用されるため、スロー回線でのスタイル未適用状態のちらつき(FOUC: Flash of Unstyled Content)を防げる点で優れている。
初回訪問時はOSの設定に従い content="light dark" を指定し、ユーザーが切り替えた際はJavaScriptでこの属性値を書き換え、localStorage に保存する構成だ:
const metaTag = document.querySelector('[name="color-scheme"]');
const savedScheme = localStorage.getItem("colorScheme");
if (savedScheme) { metaTag.setAttribute('content', savedScheme); }
btnlight.addEventListener('click', function() {
metaTag.setAttribute('content', 'light');
localStorage.setItem("colorScheme", "light");
});
btndark.addEventListener('click', function() {
metaTag.setAttribute('content', 'dark');
localStorage.setItem("colorScheme", "dark");
});
btnsystem.addEventListener('click', function() {
metaTag.setAttribute('content', 'light dark');
localStorage.removeItem("colorScheme");
});
color-scheme が効く範囲・効かない範囲
ここが実装上の最大の落とし穴だ。color-scheme が影響するのは以下の要素に限られる:
light-dark()CSS関数で指定した色・グラデーション・画像CanvasやCanvasTextなどのシステムカラー- スクロールバーの色
<button>などHTML要素のデフォルト色color-schemeに対応したiframeやSVG
一方、**prefers-color-scheme メディアクエリは color-scheme の影響を受けない**。ページ内トグルで切り替える実装では、以下のような <picture> 要素による画像切り替えが動作しない点に注意が必要だ:
<picture>
<source srcset="logo-dark.png" media="(prefers-color-scheme: dark)" />
<img src="logo-light.png" alt="Product logo" />
</picture>
ただし例外が2つある。iframeとSVGに対しては color-scheme が prefers-color-scheme を上書きできる。インラインスタイルで指定すれば想定通りに動く:
<iframe style="color-scheme: dark;" src="/example.html"></iframe>
<img style="color-scheme: dark;" src="/circle.svg" alt="">
CSSの仕様は最近更新され、color-scheme がすべてのコンテキストでメディアクエリに影響するよう変更されたが、現時点でこれを実装したブラウザはない。
Safariについても注意が必要だ。最新のSafari 27ではSVG内の prefers-color-scheme サポートが追加されたが、color-scheme はSVGのメディアクエリには影響しない(バグ報告あり)。iframeについては概ね正しく動作するようになったものの、別のバグが残存している(バグ報告参照)。
グラデーション・背景画像にも使える light-dark()
light-dark() 関数はもともと単色にしか使えなかったが、Chrome/Edge 150、Firefox 150、Safari Technology Previewからグラデーションと画像にも対応した:
.bg-gradient {
background-image: light-dark(
linear-gradient(15deg, #b9b6ff, #308dc6),
linear-gradient(15deg, #6b7495, #001339)
);
}
.bg {
background-image: light-dark(url(/lightmode.avif), url(/darkmode.avif));
}
これにより、モード切り替えに応じた背景画像の差し替えも prefers-color-scheme に頼らず実現できる。
色以外のスタイルを切り替えるワークアラウンド
ダークモードでは box-shadow が暗い背景で見えなくなるため border に差し替えたい、といったケースがある。現状のCSSにはこれを直接検出する手段がないが、@property と Style Query を組み合わせたワークアラウンドが紹介されている:
@property --usedScheme {
syntax: "<color>";
inherits: true;
initial-value: transparent;
}
body {
--usedScheme: light-dark(white, black);
}
@container style(--usedScheme: white) {
.card { box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.2); }
}
@container style(--usedScheme: black) {
.card { border: solid 1px rgb(94, 94, 94); }
}
@property で型付きのカスタムプロパティを定義し、light-dark() で解決された色をスタイルクエリで参照するというアイデアだ。CSSの if() やスタイルクエリによるよりシンプルな検出は仕様策定中であり、将来的にはこのワークアラウンド自体が不要になる可能性がある。
将来的な仕様:JavaScriptから prefers-color-scheme を上書き
将来的には、JavaScriptから prefers-color-scheme を直接上書きできる仕組みが追加される可能性がある。仕様草案とMDNのエントリが存在し、Chrome Canaryではプロトタイプが実装されているが、Safariチームは反対の立場をとっており、標準化の行方はまだ不透明だ。実現すれば本記事で紹介した多くのワークアラウンドが不要になるが、当面はメタタグと color-scheme を組み合わせた現実解が選択肢の中心となる。
詳細はDark mode with web standardsを参照していただきたい。