Best Dark Toggle System Working Way

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>