
Astro+Tailwind CSS v4でのダークモードの実装
目次
はじめに
本ブログにも最初に投降した記事高速、低コスト、シンプルなブログを作成できるAstroとFront Matter CMSの魅力で言及したダークモードをようやく実装しました。
今回は、Astro と Tailwind CSS v4 を使ったサイトにダークモードを実装する方法について解説します。0からの実装ではなく、Tailwind CSS のテーマ機能を利用していることを前提としています。
dark:bg-primary
のような指定でダークモードとの色を切り替える方法ではなく、bg-primary
の指定はそのままでテーマを切り替えることを目指します。
また、Astro の*ClientRouter(旧ViewTransitions)*を利用している場合でもスムーズに動作するような考慮点も紹介します。
Tailwind CSS部分はこちらの記事を参考にしました。

Astro単体での実装はAstro Tutorial🔗をご覧ください。
この記事の前提
この実装を進めるにあたって、以下の環境や知識があることを前提としています。
- Astro の基本的な使い方を理解していること
- Tailwind CSS バージョン4.xを使用していること
- Tailwind CSS での色の管理には、CSSファイル内の
@theme
ブロック(Tailwind CSS Theme Documentation🔗を参照)を使用していること。これにより、text-primary
やbg-primary
といったクラス名でテーマカラーを指定できる状態を想定しています。
たとえば、以下のように @theme
でカスタムプロパティを定義しているようなケースです。
@import "tailwindcss";
@theme {
--font-kosugi: "Kosugi Maru", sans-serif;
--color-primary: oklch(0.5 0.1 30);
--color-primary-content: oklch(1 0 0);
--color-base-100: oklch(0.95 0.03 75);
--color-base-200: oklch(0.95 0.038 75.164);
--color-base-300: oklch(0.9 0.076 70.697);
--color-base-content: oklch(0.4 0.123 38.172);
/* ... */
}
目指すダークモード設定
今回の実装で目指すのは、以下の2点です。
- HTMLのクラス指定で
dark:bg-primary
のようにdark:
プレフィックスを都度書くのではなく、bg-primary
のようなシンプルなクラス指定のまま、ダークモードとライトモードで色が切り替わるように - ユーザーが手動でダークモードとライトモードを切り替えられるボタンの実装
実装手順
それでは、具体的な実装手順を見ていきましょう。
1. 初期表示時のテーマ設定スクリプト
まず、ページ読み込み時にユーザーのOS設定や以前の選択に基づいてテーマを適用するスクリプトを記述します。 このスクリプトは、ページのちらつき(FOUC: Flash Of Unstyled Content)を防ぐために、headerタグの中などHTMLの早い段階で実行されるようにします。
src/components/ThemeInitializer.astro
---
---
<script is:inline>
// ダークモードの設定を読み込む
const theme = (() => {
const localStorageTheme = localStorage?.getItem("theme") ?? "";
if (["dark", "light"].includes(localStorageTheme)) {
return localStorageTheme;
}
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
return "dark";
}
return "light";
})();
if (theme === "light") {
document.documentElement.classList.remove("dark");
} else {
document.documentElement.classList.add("dark");
}
window.localStorage.setItem("theme", theme);
</script>
このスクリプトは、まずlocalStorageに保存されたテーマ設定を確認し、なければOSのダークモード設定 (prefers-color-scheme) を参照します。どちらもなければデフォルトでライトモードとします。
そして、決定したテーマに応じて html
タグにdark
クラスを付与または削除し、その選択をlocalStorageに保存します。
Layout等のhead
タグの中に組み込んでください。
src/layouts/Layout.astro
---
import ThemeInitializer from "../components/ThemeInitializer.astro";
---
<html lang="ja">
<head>
<ThemeInitializer />
{/* other head content */}
</head>
<body>
<slot />
</body>
</html>
2. テーマ切り替えボタンコンポーネント
次に、ユーザーが手動でテーマを切り替えるためのボタンコンポーネントを作成します。
ここではアイコンは自分の好きなアイコンを用意してください。ここではIcon.astro
というアイコン表示用のコンポーネントを別途用意しています。
src/components/ThemeToggle.astro
---
import Icon from "./Icon.astro";
---
<button
id="theme-toggle"
type="button"
class="text-secondary-content hover:text-accent"
aria-label="ダークモード切り替え"
>
<Icon name="mdi:theme-light-dark" size={24} />
</button>
<script is:inline>
const themeToggle = document.getElementById("theme-toggle");
if (themeToggle) {
themeToggle.addEventListener("click", () => {
const element = document.documentElement;
element.classList.toggle("dark");
const isDark = element.classList.contains("dark");
localStorage.setItem("theme", isDark ? "dark" : "light");
});
}
</script>
ボタンがクリックされるたびに html
タグのdark
クラスをトグルし、現在の状態をlocalStorageに保存します。
このテーマ切り替えボタンコンポーネントを自身のHeaderの部分に組み込んでください。ここではHeader.astro
というコンポーネントを別途用意しています。
src/components/Header.astro
---
import DarkModeToggle from "../DarkModeToggle.astro";
---
<header>
<nav>
<DarkModeToggle />
{/* other header content */}
</nav>
</header>
3. Tailwind CSS の設定
ここが今回のキモとなる部分です。CSSカスタムプロパティを使ってテーマごとの色を定義します。
@layer base
内で、:root
(ライトモード時)と.dark
(ダークモード時セレクターそれぞれに対して、これらのCSSカスタムプロパティの具体的な色値を設定します。
そして、その変数を使って@theme
ディレクティブ内で、使用する色の名前(例:--color-primary
)を定義します。
@import "tailwindcss";
@theme {
--font-kosugi: "Kosugi Maru", sans-serif;
--color-primary: var(--primary);
--color-primary-content: var(--primary-content);
--color-base-100: var(--base-100);
--color-base-200: var(--base-200);
--color-base-300: var(--base-300);
--color-base-content: var(--base-content);
/* ... */
}
@layer base {
/* ライトモード(デフォルト)のテーマ色 */
:root {
--primary: oklch(0.5 0.1 30);
--primary-content: oklch(1 0 0);
--base-100: oklch(0.95 0.03 75);
--base-200: oklch(0.95 0.038 75.164);
--base-300: oklch(0.9 0.076 70.697);
--base-content: oklch(0.4 0.123 38.172);
/* ... */
}
/* ダークモード時のテーマ色 */
.dark {
--primary: oklch(0.35 0.1 30);
--primary-content: oklch(0.91 0.0084 90);
--base-100: oklch(0.2 0.0327 67.34);
--base-200: oklch(0.28 0.0458 71.63);
--base-300: oklch(0.4 0.0564 70);
--base-content: oklch(0.91 0.0107 39.44);
/* ... */
}
}
こうすることで、bg-primary
のようなクラスを使った場合、html
タグにdark
クラスが付いているかどうかで、--primary
の値が自動的に切り替わり、結果として背景色が変わるという仕組みです。
これにより、HTML側でdark:
プレフィックスを記述する必要がなくなります。
4. [オプション] ClientRouter との連携
Astro のClientRouter(旧ViewTransitions) を使用している場合、ページ遷移はクライアント側で行われます。
このとき、is:inline
で記述されたスクリプトも、通常のページ読み込み時とは異なる挙動をすることがあります。
具体的には、テーマを適用するスクリプトやテーマを切り替えるボタンの処理が、ページ遷移後に動作しなくなる可能性があります。
そのためには、Astroが提供するライフサイクルイベントを監視して適切なタイミングでスクリプトを実行させる必要があります。
テーマを適用するロジックを関数にまとめ、astro:after-swap
イベント発生時にも呼び出されるようにします。
astro:after-swap
は、新しいページの内容がDOMに挿入された直後に発火します。
src/components/ThemeInitializer.astro
---
---
<script is:inline>
// ダークモードの設定を読み込む
+ const applyTheme = () => {
const theme = (() => {
const localStorageTheme = localStorage?.getItem("theme") ?? "";
if (["dark", "light"].includes(localStorageTheme)) {
return localStorageTheme;
}
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
return "dark";
}
return "light";
})();
if (theme === "light") {
document.documentElement.classList.remove("dark");
} else {
document.documentElement.classList.add("dark");
}
window.localStorage.setItem("theme", theme);
+ };
+ document.addEventListener("astro:after-swap", applyTheme);
applyTheme();
</script>
同様に、テーマ切り替えボタンのイベントリスナーを設定する処理も関数化し、astro:page-load
イベント発生時に呼び出します。
astro:page-load
は、すべての要素が読み込まれ、ページが完全にインタラクティブになった後に発火します。これにより、新しいページでもボタンが正しく機能するようになります。
src/components/ThemeInitializer.astro
---
import Icon from "./Icon.astro";
---
<button
id="theme-toggle"
type="button"
class="text-secondary-content hover:text-accent"
aria-label="ダークモード切り替え"
>
<Icon name="mdi:theme-light-dark" size={24} />
</button>
<script is:inline>
+ const clickHandleToggleTheme = () => {
const themeToggle = document.getElementById("theme-toggle");
if (themeToggle) {
themeToggle.addEventListener("click", () => {
const element = document.documentElement;
element.classList.toggle("dark");
const isDark = element.classList.contains("dark");
localStorage.setItem("theme", isDark ? "dark" : "light");
});
}
+ };
+ document.addEventListener("astro:page-load", clickHandleToggleTheme);
</script>

まとめ
今回は、Astro + Tailwind CSS v4 環境で、CSS Custom Properties を活用したダークモード実装方法をご紹介しました:
色の定義をCSS Custom Propertiesに集約することで、将来的なテーマの変更や追加も容易になります。 ぜひ、ご自身のAstroプロジェクトで試してみてください
参考文献
- Next.js 15 App Router で Tailwind CSS V4 を使用してダークモードを追加する方法🔗
- Astro Tutorial🔗
- Tailwind CSS Documentation: Theme🔗
- Astro Documentation: View Transitions🔗
- Astro Documentation: ページナビゲーション中のスクリプトの動作🔗