一般網頁實現深淺樣式切換,透過三重不同的 HTML 標籤實現。具體來說可以分為「淺色」,「深色」,「自動/系統」。
而我認為一個樣式美觀的網頁,在一般情況下是無須使用者手動切換的。就算需要,也無須三個樣式造成界面的混亂和隱性的抉擇成本。
更好的解決方案是,儘保留「自動/系統」,再加入一個「反向」的標籤。也就是 system 和 opposite。
詳解
當然主題的加載邏輯需要優化,好處是顯而易見的——減輕了選擇樣式的負擔,同時減弱 Toggle 的存在感。
- 獲取系統偏好並預設加載。
- 加載主題。此時如果 Session Storage 內存有用戶的偏好,則取用戶偏好而不是系統偏好。
- 監聽系統主題變動。當系統主題改變,如果當前主題為 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>