打造完美的網站深色模式切換方案

September 30, 2025

一般網頁實現深淺樣式切換,透過三重不同的 HTML 標籤實現。具體來說可以分為「淺色」,「深色」,「自動/系統」。

而我認為一個樣式美觀的網頁,在一般情況下是無須使用者手動切換的。就算需要,也無須三個樣式造成界面的混亂和隱性的抉擇成本。

更好的解決方案是,儘保留「自動/系統」,再加入一個「反向」的標籤。也就是 systemopposite

詳解

當然主題的加載邏輯需要優化,好處是顯而易見的——減輕了選擇樣式的負擔,同時減弱 Toggle 的存在感。

  1. 獲取系統偏好並預設加載。
  2. 加載主題。此時如果 Session Storage 內存有用戶的偏好,則取用戶偏好而不是系統偏好。
  3. 監聽系統主題變動。當系統主題改變,如果當前主題為 opposite,則變為 system;如果當前主題為 system,則保持 system

關於第三點,是為了應對那些手動切換了偏好,但系統主題變動導致的場景。比如傍晚你手動切換到了暗色,此時系統主題由亮色自動轉為暗色,你大概率會希望頁面配色保持暗色,所以由 opposite 變為 system

實作

我的實際 Layout.astro 中所寫的,其中 theme-toggle 是我頁面中的樣式切換按鈕。

<html lang="en">
    <head>
        <...>
        <script is:inline>
            (function () {
                const themeKey = "theme";
                const intent = sessionStorage.getItem(themeKey);
                const isSystemDark = window.matchMedia(
                    "(prefers-color-scheme: dark)",
                ).matches;

                if (
                    ((intent === "system" || !intent) && isSystemDark) ||
                    (intent === "opposite" && !isSystemDark)
                ) {
                    document.documentElement.classList.add("dark");
                }
            })();
        </script>
    </head>
</html>

<script is:inline>
    const themeKey = "theme";
    const themeQuery = "(prefers-color-scheme: dark)";

    const getStoredIntent = () => sessionStorage.getItem(themeKey);

    const getSystemTheme = () => {
        return window.matchMedia(themeQuery).matches ? "dark" : "light";
    };

    const applyTheme = (intent) => {
        const systemTheme = getSystemTheme();
        let themeToApply;
        if (intent === "opposite") {
            themeToApply = systemTheme === "dark" ? "light" : "dark";
        } else {
            themeToApply = systemTheme;
        }
        document.documentElement.classList.toggle(
            "dark",
            themeToApply === "dark",
        );
    };

    const toggleTheme = () => {
        const currentIntent = getStoredIntent() || "system";
        const newIntent = currentIntent === "system" ? "opposite" : "system";
        sessionStorage.setItem(themeKey, newIntent);
        applyTheme(newIntent);
    };

    // --- EXECUTION ---

    document.addEventListener("click", (event) => {
        if (event.target.closest("#theme-toggle")) {
            if (!document.startViewTransition) {
                toggleTheme();
                return;
            }
            document.startViewTransition(toggleTheme);
        }
    });

    document.addEventListener("astro:after-swap", () => {
        applyTheme(getStoredIntent() || "system");
    });

    window.matchMedia(themeQuery).addEventListener("change", () => {
        if (getStoredIntent() === "opposite") {
            sessionStorage.setItem(themeKey, "system");
        }
        applyTheme(getStoredIntent() || "system");
    });
</script>