CSS 入口少一行,元件的內距全部消失

Tailwind v4 的內容掃描來源是宣告出來的,不是自動的。CSS 入口少一行 library import, 該套件獨有的 utility 整批不生成,畫面卻看起來正常。這篇拆解為什麼所有既有的檢查關卡 都量不到這件事,以及唯一那行決定性的判準。

畫面看起來是正常的。元件都渲染出來、沒有 console error、沒有 build warning、沒有紅色 overlay。

拿尺量才看得出問題:

  • icon-only 按鈕的 computed padding 是 0px,命中區只有 14×44,應該是 32×44
  • 輸入框高 20px,應該是 32px
  • 圖示掉回 iconify 的預設 1em(14px),而不是 size-5 指定的 20px

在 devtools 搜尋 runtime CSS,.p-1\.5、.size-5 這些 class 根本不存在於任何一張 stylesheet。

套件已正確安裝、module 已註冊、元件 props 都對。只有樣式是空的。

根因:一行 @import

CSS 入口只有這一行:

@import 'tailwindcss';

缺了這一行:

@import '@nuxt/ui';

Tailwind v4 拿掉了 v3 的 content: [] 設定,改成宣告式的掃描來源——@import 與 @source 指令登記哪些檔案要被掃。而 @import '@nuxt/ui' 除了帶入 theme 變數,同時把 node_modules/@nuxt/ui 的元件原始碼登記為掃描來源。

少了它,Tailwind 從來沒看過那些檔案。於是只出現在元件庫內部、而專案自己從來沒寫過的 utility(.p-1\.5、.size-5 這一類)完全不會被生成。

這裡是整件事最狡猾的地方:專案自己寫過的 class 照常生成。所以整體版面仍然成立,失效範圍剛好是「你沒寫過、但元件內部有寫」的那一批——也就是元件的內距與尺寸。症狀因此表現為局部尺寸錯亂,而不是「整站沒有樣式」,離根因非常遠。

一般化的講法是:任何宣告式內容掃描的 CSS 工具,少登記一個來源時,失效的是那個來源獨有的 utility 子集,不是全部。

為什麼沒有任何一個關卡攔得到

這才是這個坑真正值得記下來的部分:

關卡它實際讀的東西對這個坑的訊號
lint原始碼的 AST零。CSS 入口的語法完全合法
typecheckTypeScript 型別零。跟型別無關
元件測試(happy-dom)DOM 結構、props、事件零。happy-dom 不計算 computed style,元件照樣掛載
build / CI有沒有錯零。build 成功,沒有 warning
人眼看畫面版面有沒有壞幾乎零。元件都在、版面成立,不拿尺量看不出來

四道自動關卡全綠,加上人眼看起來正常——這個缺口可以在一個專案裡活很久。

而判別成本只有一行:檢查 runtime CSS 裡有沒有 .p-1\.5。

這個落差本身就是重點。一個「所有檢查都過、但只要一行就能判定」的問題之所以能長期存在,不是因為它難,是因為沒有任何一道關卡的觀測對象是產物本身。lint 讀原始碼、typecheck 讀型別、單元測試讀 DOM 結構——沒有人讀最終那份 CSS。

怎麼判定

先確認專案真的依賴那個 UI 套件,再檢查 CSS 入口。

關鍵是不要寫死入口路徑。monorepo 的 CSS 入口可能在某個 layer 或 package 底下,要從 nuxt.config 宣告的 css 欄位去取:

# 取得真正的入口路徑,不要假設是 app/assets/css/main.css
grep -n "css:" nuxt.config.ts

# 所有 import 了 tailwindcss 但沒 import UI 套件的 CSS 檔
grep -rlE "^@import\s+['\"]tailwindcss['\"];" --include='*.css' . \
  | grep -v node_modules | grep -v '/.nuxt/' \
  | while read -r f; do
      grep -qE "^@import\s+['\"]@nuxt/ui['\"];" "$f" || echo "AFFECTED: $f"
    done

跑起來之後,決定性的驗證仍然是開 devtools 搜 runtime CSS——上面這段只是把候選縮小。

副本會自己漂掉

還有第二個面向值得一提。

同一份 CSS 入口在一個 repo 裡如果存在兩份副本(比如專案本體一份、專案樣板一份),被人維護的永遠只有本體那份。樣板那份會靜靜地漂掉,而每一個用該樣板生出來的新專案都帶著這個缺口。

副本之間沒有任何機械比對,所以漂移不會被發現——直到某個新專案的按鈕內距是 0。

這類問題的處理原則很單純:同一份設定不要有兩個維護點。做不到就補一條機械比對,讓兩份副本不一致時有東西會叫。靠人記得同步兩個地方,在時間尺度上等於沒有處理。

升級 Tailwind v4 時該一起確認的事

如果你正在做 v3 到 v4 的升級,content: [] 消失是最容易被忽略的一項改變。v3 的預設行為傾向於「掃得比較多」,v4 傾向於「你說什麼我掃什麼」。

升級之後值得逐項確認:

  1. 每一個第三方元件庫都有對應的 @import 或 @source 登記
  2. 不在標準掃描路徑下的自家程式碼(monorepo 的其他 package、動態產生的樣板)也有登記
  3. 驗收看產物,不要看 build 有沒有過。開一個用到該元件庫的頁面,量一個元件的 computed style

第三點是唯一有鑑別力的。前兩點是清單,清單能幫你少漏,但只有量產物能告訴你有沒有漏。