
Tailwind CSS 最大的价值之一,不只是它提供了大量现成的原子类,而是它帮我建立了一套稳定的样式思维方式。开发 UI 时,我不再反复纠结“这个组件该起什么 class 名”“这段样式应该写在什么层级”,而是围绕布局、间距、颜色、状态去直接表达界面。
Tailwind CSS 最有價值的地方之一,不只是它提供了大量現成可用的原子類別,更重要的是,它幫我建立起一套穩定的樣式思考方式。開發 UI 時,我不需要一直反覆思考「這個元件該取什麼 class 名稱」、「這段樣式應該寫在哪一層」,而是能直接從版面、間距、色彩與狀態出發,表達畫面想呈現的樣子。
TW 真的是很了不起的創意!
不過,真實專案不會永遠停留在「把原子類別一路疊上去就好」的階段。
當頁面中開始出現一組外觀高度一致、但結構又還不到值得抽成元件的元素時,樣式該怎麼整理,就會變成另一個需要面對的問題:
這篇文章想討論的,就是一個很具體的問題:在 Tailwind CSS v4 裡,該怎麼在不破壞 utility-first 工作流程的前提下,定義可重複使用的樣式。
我會結合 block-style-cms 裡的一個實際案例,說明我最後為什麼選擇在全域樣式中定義自訂 utility,以及這種做法帶來的好處、適用情境與實作時要注意的地方。
在 block-style-cms 中,使用者聯絡方式區塊裡有多個輸入欄位。這些欄位承載的資料不同,但視覺呈現幾乎一致。

如果完全依照 Tailwind 的原子類別寫法,每個輸入框可能都會帶上一長串 class:
這樣寫本身完全沒問題,而且在很多情況下,這確實就是最直接、效率也最高的做法。
問題在於,當同一組樣式在不同位置反覆出現之後,後續的維護成本就會逐步浮現:
這時候真正要處理的問題,已經不是「Tailwind 能不能寫得出來」,而是「要怎麼在 Tailwind 的生態裡,更優雅地管理重複樣式」。
在這個情境下,我大致評估過三種方案。
這是最符合 Tailwind 預設工作流程的做法。
它的優點很明確:
但缺點也很清楚:
如果這些輸入框不只樣式一致,連結構、互動與行為也都一致,那麼抽成元件通常會是正確選擇。
但在這個案例裡,我要處理的核心其實不是元件抽象,而是樣式抽象。
如果只是為了重用這幾個 className 就包成一個元件,通常會讓元件背上太多責任:
換句話說,元件重用解決的是結構問題,未必適合單純的樣式重用問題。
這是我最後採用的做法。
它的核心思路不是「離開 Tailwind,另外寫一套 CSS」,而是「在 Tailwind 的設計系統裡,擴充 Tailwind 本身」。
這種方式特別適合下面這類情境:
目前這個專案使用 Next.js,整體樣式入口是 globals.css(有些專案可能會叫做 app.css 或 styles.css)。這個位置剛好很適合放專案層級、可重複使用的樣式。
在 Tailwind CSS v4 中,官方也更建議使用 @utility 來定義自訂工具類別。相較於舊語境中的 @layer utilities 寫法,@utility 的語意更直接,也更符合 v4 的 CSS-first 設定方式。
例如,我把上面那組重複出現的輸入框樣式整理成一個 utility:
實際使用時,元件裡的 className 就會乾淨很多:
如果某些地方還需要局部覆寫,也一樣可以繼續疊加原子類別:
這篇文章的重點不只是「可以這樣寫」,而是「為什麼在 v4 裡,這樣做是合理的」。
Tailwind CSS v4 一個很重要的變化,是它更強調 CSS-first 設定與樣式變數。像 --spacing、--radius-lg、--color-gray-300 這類變數,本質上就是專案設計系統的一部分。
也就是說,當你透過這些變數來定義自訂 utility 時,你其實沒有離開 Tailwind,反而是在沿用它提供的統一設計基礎。
這樣做會有幾個很直接的好處。
這點非常重要。
如果你完全在外部再寫一套一般 CSS 值,例如直接寫死 8px、12px、#d1d5db,短期看起來也能正常運作,但長期來看,它會慢慢形成第二套樣式來源。
相對地,若是基於 v4 的主題變數來定義 utility,就代表:
未來如果主題有調整,這些 utility 也能自然維持一致性。
原本散落在多個 JSX 檔案裡的 className,現在可以收斂成單一入口。
之後不管是邊框顏色、輸入框 padding,還是 focus 狀態,只要改一個地方就好,不需要再到多個元件裡搜尋與替換。
這也是我在業務程式碼裡很重視的一件事。
當 JSX 中塞滿一長串樣式時,你雖然知道那些都是 Tailwind class,但真正的業務語意很容易被埋掉。把重複的視覺規則抽到 utility 之後,元件本身就能更專注在結構與行為上。
很多時候,我們會很直覺地把「重複樣式」理解成「應該抽元件」。
但這兩件事其實不一定相等。
自訂 utility 提供的是一種中間層方案:
對真實專案開發來說,這通常是一個很實用的平衡點。
從結果來看,自訂 utility 的確也是一個 CSS 類別。但我想強調的是,這種寫法和「完全脫離 Tailwind,自己寫一套樣式」其實不是同一件事。
差別在於:我不是把它當成回到傳統語意化 CSS 的做法,而是把它視為 Tailwind 體系中的一層專案級封裝。
如果整個寫法仍然是圍繞 Tailwind v4 的設計變數展開,那它本質上還是服從同一套設計語言。
可以用一個簡單對比來理解:
這也是為什麼我更傾向把它理解成「在 Tailwind 裡擴充 Tailwind」,而不是「繞過 Tailwind」。
任何抽象一旦超出邊界,就會開始製造新的問題。
我認為,自訂 utility 比較適合下面這些情況:
相反地,下面這些情況就不一定適合:
一個簡單的判斷標準是:
如果你重用的是「視覺規則」,優先考慮 custom utility;如果你重用的是「結構與行為」,優先考慮元件。
在定義 custom utility 時,你寫的是原生 CSS 規則,不是再把 Tailwind 原子類別拼回 CSS 裡。
也就是說,這裡要重用的是 Tailwind 的設計 token 與整體體系,而不是把 HTML 裡的 utility class 直接搬進 CSS。
這點很重要,因為它會決定你的抽象是不是仍然清楚:
如果這兩者的邊界不清楚,之後很容易又回到難以維護的樣式組織方式。
Tailwind CSS 的目標,從來不是拒絕抽象,而是避免失控的抽象。
當專案裡開始出現重複樣式時,最直接的做法通常還是繼續寫原子類別;但當重複累積到一定程度之後,單靠複製 className 就不再是最好的解法。
在 Tailwind CSS v4 中,透過 @utility 定義自訂工具類別,是一種很實用的中間方案:
對我來說,這種做法最有價值的地方,不只是程式碼變短,而是它讓我在真實專案中,可以更穩定地管理樣式隨著需求擴張時的複雜度。
如果你的專案裡也開始出現「同類型樣式反覆出現,但暫時又還不到值得抽成元件」的情況,那麼在 Tailwind CSS v4 中定義 custom utility,會是一個很值得優先考慮的方向。