深色模式的防閃爍:一段必須跑在最前面的腳本
深色模式的難點不在 CSS——變數換值而已——在於 SSR 的第一幀。伺服器不知道訪客偏好哪個主題,吐出的 HTML 只能預設淺色;等 JS 掛載後讀 localStorage 再切換,深色使用者就會看到「白屏一閃、再轉黑」。這一閃在深夜的螢幕上刺眼得像個 bug。
解法:在首次繪製前釘住主題
一段幾行的 inline script,放在 <head>、任何樣式表和框架之前執行:讀 localStorage 的使用者選擇(沒有就問系統偏好 prefers-color-scheme),把 data-theme 直接寫在 <html> 上。瀏覽器開始繪製時主題已經定案,一幀都不會錯。
html
<script>
(function () {
var t = localStorage.getItem('theme');
if (t !== 'dark' && t !== 'light')
t = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', t);
})();
</script>
整站的顏色全走 CSS 變數(單一 tokens 檔),兩個主題就是兩組變數值——data-theme 一釘,所有顏色瞬間就位。
Hydration 的第二個坑
Vue 的狀態在 SSR 時不知道主題,初值只能設淺色;如果 UI 上有依主題渲染的文字(切換鈕的「亮/暗」標籤),server 版與 client 版會對不上——hydration mismatch。解法分兩層:顏色類的呈現全部交給 CSS 選擇器(跟著 data-theme 走,不經過 JS 狀態);文字類的包 <ClientOnly> 延後渲染。主題切換鈕的日月圖形因此是純 CSS 的兩個相位——狀態在 DOM 屬性上,不在元件裡。
原則濃縮成一句:主題是文件層級的狀態,不是元件狀態。放對層級,閃爍與 mismatch 都不藥而癒。
讀到這裡若有一點收穫,替這顆種子澆一次水——匿名、一人一次。