半透明深色卡被淺色底洗淡、讀主題的函式在第一次 render 拿不到值。console 全綠、版面沒壞、只有字不見了。附四個檢查點。
發布於 2026-09-06・約 5 分鐘
我的站上 33 個工具,其中 19 個是淺色主題。共用元件、共用 CSS class、漸層掃光——大多是照深色頁做的,因為第一個做的工具是深色的。
結果是同一個 bug 反覆發作五次:
2026-08-28 導流卡在 19 個淺色頁,對比 1.12–1.24 2026-08-31 三個工具的長條標籤整區隱形 2026-09-03 分享頁的結果橫幅對比 1.08 2026-09-03 某個漸層的最亮色停對比 1.00(跟背景同色) 2026-09-03 半透明深色卡被洗淡,卡內文字 2.53、按鈕 3.38
症狀不是「有點怪」,是文字直接消失。而且 console 全綠、版面沒壞、沒有任何錯誤。
問題出在這種寫法:
.oracle-card { background: rgba(14, 15, 42, 0.82); } .product-card { background: rgba(12, 13, 38, 0.65); }
疊在深色頁上,合成出來還是深色,設計預期成立。疊在近白色的頁面上,合成出來是灰藍色——卡片被洗淡了,而卡裡的文字色是照「真正的深色卡」挑的。
修法有兩條路,我選了看起來比較怪的那條:
/* ❌ 把卡改成淺色 */ /* ✅ 淺色工具在根元素覆寫成不透明的同色 */ [data-tone="light"] { --card-bg: rgb(14, 15, 42); --product-card-bg: rgb(12, 13, 38); }
深色卡壓在淺色頁上,是那些頁面既有的視覺選擇,本來就是刻意的對比。問題只在於它是半透明的。把卡改成淺色會改掉設計,把它改成不透明只是讓它變回設計原本要的樣子。
判斷這種修法選哪條的方式:問「原本的視覺意圖是什麼」。如果深色卡在深色頁上看起來也是深色卡,那意圖就是「深色卡」,透明度只是實作細節。
這個最隱蔽,也最貴。
我有一個 readTone() 讀 <html data-tone>,而那個屬性是某個共用元件用 useLayoutEffect 設的:
readTone()
<html data-tone>
useLayoutEffect
// BackHome.jsx useLayoutEffect(() => { document.documentElement.dataset.tone = TOOLS[toolId].tone }, [toolId])
平常沒事。從分享連結(?r=xxx)進來時就出事了:
?r=xxx
結果畫面在第一次 render 就出現,跟設定 tone 的元件在同一個 commit 裡。effect 還沒跑,readTone() 回傳預設值 dark,深色配色套在淺色頁上——而且不會再重繪一次。
dark
原本的程式碼裡有一句很有自信的註解:「這些元件只在結果頁出現,那時早就 commit 過了」。
那個假設在正常導覽下成立,在分享連結這條路上不成立。而分享正是最高價值的入口——那是別人第一次看到這個站的地方。
修法是把時序依賴整個拿掉:
function readTone(toolId) { const attr = document.documentElement.dataset.tone if (attr) return attr return TOOLS_META[toolId]?.tone ?? 'light' // 讀不到就直接查靜態資料 }
不要等 effect,直接去問資料來源。
每次做新的共用元件,我現在會過一遍這四題:
一、它有沒有半透明的深色底? → 在淺色頁會被洗淡,卡內文字會跟著失效。
二、它有沒有漸層文字或掃光? → 每個色停都要在淺色底上量過。.gold-text 那種是深色頁專用的,最亮的色停可能跟淺色背景同色,量出來對比 1.00。
.gold-text
三、它吃不吃 readTone() 這類讀 DOM 的函式? → 確認在深連結的第一次 render 就拿得到正確值,不要依賴 effect 順序。
四、它有沒有把「識別色」當文字色? → 這條最常被忽略。結果頁的強調色(result.accentHex、MBTI 的 16 色)是挑來配插畫的,不是挑來當白底文字的。
result.accentHex
/* 識別色給邊框,文字回墨色 */ .result-card { border-color: var(--accent); color: var(--ink); } /* 真的要用識別色當文字,先壓深 */ .result-title { color: color-mix(in srgb, var(--accent) 55%, #000); }
這個病還有一個變體,跟顏色無關。
HeroScene 帶著全頁唯一的 <h1>,而 intro / quiz / result 三個畫面是互斥渲染的。結果畫面把 hero 換掉,h1 就跟著消失,頂層標題退成 h2,有一個工具甚至是 h3。
HeroScene
<h1>
而那個結果網址(?r=xxx)正是被分享、被搜尋引擎收錄的網址。
33 個工具有 8 個中招。判準不是「檔案裡有沒有 <h1> 標籤」——用 grep 找 <h1> 每一頁都有。判準是**「結果那一屏,hero 還在不在」**:
hero: true → false 會掉 h1(8 個工具) hero 一直都在 h1 一直在,本來就對
要驗這種東西只能真的驅動 UI 走到那一屏再量。我寫了一支 Playwright 腳本填輸入框、點 CTA、量每一階段的 h1 數量。靜態分析看不出來,因為問題出在「哪個分支被渲染」。
因為每次修的都是症狀,不是成因。
成因是:共用元件承載了一個沒有被寫下來的假設(「背景是深色的」),而使用它的地方有 19 個不符合那個假設。每加一個新元件,就多一次中獎機會。
真正的修法有兩種,我兩種都做了:
一、把假設變成 token。 元件不直接寫顏色,只寫 var(--card-bg) / var(--ink),由頁面層決定實際值。這樣新元件天生就跟著頁面走。
var(--card-bg)
var(--ink)
二、把檢查自動化。 上面那四個問題,前兩個可以用腳本掃(量每個元件在淺色頁上的實際對比),第三個可以寫測試(用深連結渲染再檢查),第四個要靠 code review。
第一種治本,但改造既有元件要時間;第二種在改造完成前先擋住新的中獎。
標籤:React、CSS、主題、無障礙、對比