バージョン
Astro 4.13.1 で確認している内容です。
Astro のダークモードについて
このサイトではダークモードの実装と切替ができるボタンを配置しています。
Astro には公式ドキュメントにて導入方法が載っているので、そちらを雛形にしました。
Viewtransitions を導入しない場合は上記だけで問題ないです。
ただページ間のシームレスな移動が当たり前に普及している昨今では、つい導入したくなる Viewtransitions を導入すると、それに対応した記述が少し必要です。
実装
以下はこのサイトでの実装です。
tailwind.config.mjs で darkMode を class にしている前提にしています。
またアイコンは react-icons を導入して置き換えています。
---
import { FiSun, FiMoon } from "react-icons/fi";
interface Props {
hasText?: boolean;
}
const { hasText = false } = Astro.props;
---
<div class={`relative group/ThemeIcon ${hasText ? "has-text" : ""}`}>
<button id="themeToggle" class="absolute inset-0 rounded-full overflow-hidden" transition:persist aria-label="外観を切り替える">
<div class="group-[.has-text]/ThemeIcon:!hidden w-full h-full dark:hidden" data-tooltip-id="global-tooltip" data-tooltip-content="ページを暗くする"></div>
<div class="group-[.has-text]/ThemeIcon:!hidden w-full h-full hidden dark:block" data-tooltip-id="global-tooltip" data-tooltip-content="ページを明るくする"></div>
</button>
{hasText ? (
<div class="flex items-center gap-2 border py-2 px-4 rounded-full">
<FiSun className="dark:hidden text-xl" />
<div class="dark:hidden">ページを暗くする</div>
<FiMoon className="hidden dark:block text-xl" />
<div class="hidden dark:block">ページを明るくする</div>
</div>
) : (
<FiSun className="dark:hidden text-2xl" />
<FiMoon className="hidden dark:block text-2xl" />
)}
</div>
<script is:inline>
function setDarkMode(document) {
const theme = (() => {
if (typeof localStorage !== "undefined" && localStorage.getItem("theme")) {
return localStorage.getItem("theme");
}
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");
}
localStorage.setItem("theme", theme);
}
setDarkMode(document);
document.addEventListener("astro:before-swap", event => {
setDarkMode(event.newDocument);
});
const handleThemeToggleClick = () => {
const element = document.documentElement;
element.classList.toggle("dark");
const isDark = element.classList.contains("dark");
localStorage.setItem("theme", isDark ? "dark" : "light");
};
document.getElementById("themeToggle").addEventListener("click", handleThemeToggleClick);
if ('ontouchstart' in window || navigator.maxTouchPoints) {
document.getElementById("themeToggle").querySelectorAll("div").forEach(div => {
div.style.display = "none";
});
}
</script>
ミソは <button> タグの transition:persist と document.addEventListener の astro:before-swap です。
transition:persist ディレクティブを使用すると、ページ間のナビゲーションでコンポーネントとHTML要素を(置き換えるのではなく)保持できます。
これにより <button> タグが置き換えられないので Viewtransitions 環境下でもクリックイベントが維持されるようになります。
また document.addEventListener の astro:before-swap はページ移動時に毎回するイベントです。
こちらでダークモードかどうかを確認する処理を入れないと、ダークモード中なのにライトモードに変わってしまった!などのちぐはぐな表示になってしまいます。
ボタンの表示切り分けについては、tailwind の group を利用して表示・非表示で対応しました。
<!-- トップページではテキストありの表示。 -->
<ThemeIcon hasText={true} />
<!-- その他のページではテキストなしの表示。 -->
<ThemeIcon />
これで ViewTransitions 対応のダークモード実装とボタンの表示切り分けができました。
余談
Astro のインテグレーション機能を使って React で書くと分かりやすく書けそうですが、Astro アイランドで完結させたかったので、この実装となりました。
