家計簿アプリを作る #57:グラフのダークモード対応
家計簿アプリ作成シリーズの第57回です。ダークモード自体は#31で対応済みでしたが、その後追加したChart.jsのグラフは軸ラベルや目盛り線の色が固定のままで、ダークモード時に見えにくくなっていました。グラフもダークモードに追従するようにします。
ダークモード判定はMutationObserverで
matchMedia('(prefers-color-scheme: dark)') はOSの設定しか検知できず、アプリの DarkMode コンポーネントによる手動切り替え(OS設定を上書きする)には対応できません。DarkMode コンポーネントは <html> に dark クラスを付け外ししているだけなので、MutationObserver でこのクラス変化を直接監視する方法にしました。
$state/$effectを使う共通ロジックは.svelte.tsにする
Svelte 5のrunes($state, $effect)は .svelte ファイル以外では使えず、共通ロジックとして切り出す場合は拡張子を .svelte.ts にする必要があります。
// src/lib/utils/darkMode.svelte.ts
export function createDarkModeState() {
let isDark = $state(false);
$effect(() => {
isDark = document.documentElement.classList.contains('dark');
const observer = new MutationObserver(() => {
isDark = document.documentElement.classList.contains('dark');
});
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
return () => observer.disconnect();
});
return {
get isDark() {
return isDark;
}
};
}
$effect のクリーンアップ関数(return () => observer.disconnect())で監視を確実に解除しているのがポイントです。importするときは .svelte.ts の最後の .ts は書きません。
import { createDarkModeState } from "$lib/utils/darkMode.svelte";
const darkMode = createDarkModeState();
グラフオプションを関数化して使い回す
各グラフごとに options を個別に書くのではなく、タイトル文言だけを引数に取る関数を用意し、共通の色設定(ダークモード時は明るい色、通常時は暗い色)を一箇所にまとめました。
function buildChartOptions(titleText: string) {
const color = darkMode.isDark ? "#e5e7eb" : "#374151";
const gridColor = darkMode.isDark ? "#374151" : "#e5e7eb";
return {
plugins: {
legend: { labels: { color } },
title: { display: true, text: titleText, color },
},
scales: {
x: { ticks: { color }, grid: { color: gridColor } },
y: { ticks: { color }, grid: { color: gridColor } },
},
};
}
<Bar data={chartData} options={buildChartOptions("月次収支")} />
<Bar data={monthlyData} options={buildChartOptions("月次推移")} />
<Bar data={budgetChartData} options={buildChartOptions("カテゴリ別予算消化状況")} />
関数呼び出しはテンプレートが再描画されるたびに評価されるため、$derived にしなくても darkMode.isDark の最新値がそのまま反映されます。収支一覧ページの3つのグラフと、年間サマリーページのグラフの両方に同じ関数を適用し、実装を揃えました。
まとめ
| ポイント | 内容 |
|---|---|
| ダークモード判定 | matchMediaではなくMutationObserverで<html>のclass変化を直接監視する |
| runesの共通化 | $state/$effectを使う共通ロジックは.svelte.tsファイルにする |
| オプションの使い回し | タイトル文言だけを引数にした関数でグラフごとのoptionsを組み立てる |