ダークモードは、ボタンで切り替えるだけなら数行で作れます。ただし、そのまま公開すると読み込みの瞬間に白い画面が光る、OSの設定を変えても追従しない、といった細かな不満が出ます。この記事では、Vue 3で「ライト」「ダーク」「OSに合わせる」の3択を持ち、ちらつきも抑えた実装を解説します。
方針は4つです。①色をCSS変数にして、html要素のdarkクラスで値を切り替える、②状態はcomposableに集約する、③選択をlocalStorageに保存する、④Vueが起動する前に、保存済みのテーマを適用してちらつきを防ぐ。
CSSをテーマごとに用意する
色を直接書かずにCSS変数にしておくと、テーマの追加や色の調整が1か所で済みます。:root.darkのときだけ、変数の値を差し替えます。
/* 色は変数にして、テーマごとに値だけ差し替える */
:root {
--bg: #ffffff;
--text: #1f2937;
--border: #d1d5db;
}
:root.dark {
--bg: #111827;
--text: #f3f4f6;
--border: #374151;
}
body {
background-color: var(--bg);
color: var(--text);
}
状態をcomposableにまとめる
初期化と切り替えを別々のコンポーネントに書くと、状態がずれやすくなります。テーマの状態、保存、OS設定の監視を1つのファイルに集めます。
// composables/useTheme.js
import { ref, computed, watch, watchEffect } from 'vue'
const KEY = 'theme'
const MODES = ['light', 'dark', 'system']
function readStored() {
try {
const v = localStorage.getItem(KEY)
return MODES.includes(v) ? v : 'system'
} catch {
return 'system' // プライベートモードなどでlocalStorageが使えない場合
}
}
const mode = ref(readStored())
// OSの設定を監視する(OS側で切り替わったら追従する)
const mql = window.matchMedia('(prefers-color-scheme: dark)')
const systemDark = ref(mql.matches)
mql.addEventListener('change', (e) => {
systemDark.value = e.matches
})
const isDark = computed(
() => mode.value === 'dark' || (mode.value === 'system' && systemDark.value)
)
// html要素に反映する(他のクラスは消さない)
watchEffect(() => {
const root = document.documentElement
root.classList.toggle('dark', isDark.value)
root.style.colorScheme = isDark.value ? 'dark' : 'light'
})
// 選択を保存する
watch(mode, (v) => {
try {
localStorage.setItem(KEY, v)
} catch {}
})
export function useTheme() {
return { mode, isDark }
}
ポイントは次のとおりです。
modeがsystemのときは、matchMediaの結果に従います。OSの設定が変わると、changeイベントで自動的に追従します。html要素にdarkクラスを付けるとともに、color-schemeも設定しています。これで、スクロールバーやフォーム部品も、ダーク用の見た目になります。localStorageは、プライベートモードなどで例外になることがあるため、try/catchで囲んでいます。- この書き方は、モジュールの読み込み時に
windowを使います。Nuxtなどのサーバーサイドで動かす場合は、クライアント側でだけ実行するようにしてください。
composableの考え方はComposable関数でロジックを共通化する方法で詳しく解説しています。
切り替えUIを作る
3択なので、トグルボタンではなくラジオボタンにしています。どのコンポーネントからでも、useTheme()を呼ぶだけで同じ状態を扱えます。
<script setup>
import { useTheme } from '@/composables/useTheme'
const { mode } = useTheme()
const options = [
{ value: 'light', label: 'ライト' },
{ value: 'dark', label: 'ダーク' },
{ value: 'system', label: 'OSに合わせる' }
]
</script>
<template>
<div role="radiogroup" aria-label="テーマ">
<label v-for="o in options" :key="o.value">
<input type="radio" v-model="mode" :value="o.value" />
{{ o.label }}
</label>
</div>
</template>
v-modelの仕組みはv-modelの仕組みを完全理解をご覧ください。
読み込み時のちらつきを防ぐ
テーマをonMountedで適用すると、Vueが起動するまでの間、ライトモードで表示されます。ダークを選んでいる人には、一瞬だけ白い画面が光って見えます。これを防ぐには、index.htmlの<head>に、保存済みのテーマを先に適用する小さなスクリプトを置きます。
<!-- index.html の <head> 内。Vueが起動する前に実行する -->
<script>
(function () {
try {
var t = localStorage.getItem('theme')
var dark = t === 'dark' ||
((t === null || t === 'system') &&
window.matchMedia('(prefers-color-scheme: dark)').matches)
if (dark) document.documentElement.classList.add('dark')
} catch (e) {}
})()
</script>
ここでは、Vueの外で動くため、composableは使えません。判定の中身は、useTheme.jsと同じにしておきます。
やってはいけない書き方
旧来の解説でよく見かけるのが、document.body.className = 'dark'のように、クラスを丸ごと代入する書き方です。bodyに付いている他のクラスまで消えてしまいます。
// NG: body の他のクラスがすべて消える
document.body.className = 'dark'
// OK: darkクラスだけを付け外しする
document.documentElement.classList.toggle('dark', true)
Tailwind CSSを使っている場合
Tailwindのdark:は、標準ではOSの設定に従います。クラスで切り替えるには、設定が必要で、バージョンによって書き方が違います。
/* Tailwind v3:tailwind.config.js */
module.exports = {
darkMode: 'class'
}
/* Tailwind v4:CSSファイルの中で指定する */
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
どちらも、html要素にdarkクラスが付いたときにdark:が有効になります。上のcomposableと、そのまま組み合わせられます。
ダークモードで見落としやすい点
- 画像やロゴ:白背景を前提にした画像は、ダークで浮いて見えます。透過PNGやSVGの
currentColorを使うと対応しやすくなります。 - コントラスト:純粋な黒(#000)と白(#fff)の組み合わせは、まぶしく感じやすくなります。やや柔らかい色にすると読みやすくなります。
- 外部のウィジェット:埋め込みの地図やコメント欄などは、別途テーマの指定が必要な場合があります。
クラスの出し分けは動的クラスとスタイルを切り替える方法、保存の考え方はローカルストレージを使った状態管理とPinia × LocalStorageで永続的な状態管理を行う方法もあわせてご覧ください。
よくある質問(FAQ)
index.htmlの<head>にインラインスクリプトを置き、描画前にdarkクラスを付けてください。lightかdarkだと、手動の選択が優先されます。「OSに合わせる」を選んでいる場合だけ、matchMediaのchangeイベントで追従します。color-schemeが指定されていないためです。html要素にcolor-scheme: darkを設定すると、ブラウザ標準の部品もダーク用になります。@media (prefers-color-scheme: dark)で対応できます。ユーザーが手動で切り替えられるようにするには、クラスなどの状態と、その保存が必要です。まとめ
ダークモードの実装は、色をCSS変数にして、html要素のdarkクラスで切り替えるのが基本です。そのうえで、「OSに合わせる」を含む3択にし、OS設定の変更への追従、保存、index.htmlでの先行適用を入れると、使い心地が整います。color-schemeの指定も忘れずに行ってください。

