假設你正在維護一個文件網站。首頁的文章卡片有細邊框、圓角與留白;旁邊的版本提示,也有幾乎一樣的外框。你把那幾行 CSS 複製過去,改了邊框顏色,兩個區塊便各自成立。
幾週後,設計調整了整站的容器間距。卡片改好了,提示區塊卻漏了一處。這時很容易想:如果當初抽成 mixin,就不會忘記了。
但也可能出現另一種情況:提示區塊需要更緊湊,卡片卻應該保留舒適的閱讀空間。若兩者已經綁在同一份抽象裡,一次修改反而會波及不該改的地方。
重複的程式碼提供了一條線索,接下來仍要判斷:這些樣式是否代表同一個規則,未來是否應該一起改變? 原生 CSS @mixin 值得關注,正是因為它讓我們有機會替一組樣式規則命名,並在使用的地方傳入差異。
先找出需要一起變動的規則
沿用這個假設中的文件網站,先把需求說得具體一點:文章卡片與版本提示都需要一層安靜的外框,圓角一致;邊框與標題使用相同的強調色,提示可以有比較緊湊的間距。
兩種容器承載的內容不同,卻共享一部分視覺規則。問題不只是如何消除那幾行重複,而是哪些決定應該有共同來源,哪些應該留在各自的元件裡。
例如,圓角代表整站的外觀約定,可以集中管理;內距則可能與內容密度有關。至於卡片要不要顯示摘要、提示是否需要操作連結,都是各自的責任,沒有必要為了共用外框一起收進來。
這個區分會決定抽象的大小。如果連「共用的是什麼」都還說不清楚,先保留幾行重複通常比較容易調整。等使用情境出現,才有依據把真正穩定的部分收在一起。
自訂屬性與共用 class,已經能走多遠?
先不用 mixin。以下 HTML 是後續範例共同的結構:
<article class="site-card">
<h2 class="site-panel-title">安裝指南</h2>
<p>從專案的第一個頁面開始。</p>
</article>
<aside class="site-notice">
<h2 class="site-panel-title">版本提醒</h2>
<p>升級前請先閱讀變更紀錄。</p>
</aside>只靠選擇器分組與自訂屬性,就能把外框集中起來。這段使用的是既有 CSS:
:root {
--panel-radius: 0.5rem;
}
.site-card,
.site-notice {
border: 1px solid var(--panel-accent, #777777);
border-radius: var(--panel-radius);
padding: var(--panel-space, 1.5rem);
}
.site-card > .site-panel-title,
.site-notice > .site-panel-title {
margin-block: 0 0.75rem;
color: var(--panel-accent, #777777);
}
.site-notice {
--panel-accent: #45687d;
--panel-space: 1rem;
}這裡有兩種重用:分組的選擇器讓不同元素共用宣告,自訂屬性則提供可替換的值。var() 的工作是在屬性值裡帶入變數;它本身不會把一整組 border、padding 與巢狀規則插進另一條規則。自訂屬性規格定義了這個替換機制。
如果能調整 HTML,也可以把共同規則改放在 .site-panel,分別使用 class="site-panel site-card" 與 class="site-panel site-notice"。共用 class 表達外框,另一個 class 表達元件自己的角色;自訂屬性繼續控制顏色與內距。
對這個只有兩種容器的小網站,我會先選這種做法。宣告集中、差異看得見,也沒有額外的呼叫關係需要追查。日後增加第三種容器,如果它確實共享這層外框,加入同一個 class 就很清楚。
當共用規則要在多個元件樣式裡組合、各處需要明確傳入不同設定,或不方便替不同來源的 HTML 都加上共同 class 時,mixin 才提供另一種組織方式。它讓套用的位置留在 CSS 中,透過名稱表達元件選用了哪一組規則。
| 想重用的內容 | 可以先考慮 | 需要維護的關係 |
|---|---|---|
| 顏色、間距、圓角等值 | 自訂屬性 | 變數的來源、繼承與覆寫 |
| 一組完全相同的宣告 | 共用 class 或選擇器分組 | 哪些元素應該共同接受修改 |
| 帶參數、可在不同規則中組合的樣式區塊 | 原生 mixin 草案 | 呼叫位置、參數與區塊的責任 |
三者可以搭配使用。mixin 裡仍然可以引用設計 token,共用 class 也可以是 mixin 的套用位置。選擇的重點是讓變更的責任更清楚。
用 @mixin 定義規則,在 @apply 的位置套用
把外框改寫成 mixin,最小形式如下。這段與後續的 mixin 範例都屬於草案語法:
/* 草案語法:定義與套用 */
@mixin --panel-frame() {
border: 1px solid #777777;
border-radius: 0.5rem;
padding: 1.5rem;
}
.site-card {
@apply --panel-frame();
}
.site-notice {
@apply --panel-frame();
}@mixin 定義可重用的區塊,名稱以 -- 開頭;@apply 則指定套用的位置。依草案的定義與套用規則,mixin 定義不能放在一般樣式規則內,@apply 則用在樣式規則或其中的巢狀群組規則內。上例因此將定義放在頂層,把呼叫放進各自的選擇器。
可以先把它理解成:在呼叫位置引入那組宣告與規則。不過,這只是幫助閱讀的模型;涉及參數、變數與作用範圍時,不能把它當成任意文字的複製貼上。
沒有參數時,目前草案允許省略括號,例如 @apply --panel-frame;。本文保留括號,方便辨識呼叫,也讓下一步增加參數時保持一致。這不是把 class 名稱傳給 @apply;這裡引用的是定義好的 mixin 名稱。
接著,把強調色與內距列為可調整的部分。下例是取代上一版定義的完整 CSS,繼續搭配前面的 HTML:
/* 草案語法:參數、預設值與巢狀規則 */
@mixin --panel-frame(
--accent <color>: #777777,
--space <length>: 1.5rem
) {
border: 1px solid var(--accent);
border-radius: 0.5rem;
padding: var(--space);
& > .site-panel-title {
margin-block: 0 0.75rem;
color: var(--accent);
}
}
.site-card {
@apply --panel-frame();
}
.site-notice {
@apply --panel-frame(#45687d, 1rem);
}--accent 接受顏色,--space 接受長度;冒號後面是省略引數時使用的預設值。卡片採用預設設定,提示則傳入藍灰色與較小的內距。型別與預設值的寫法沿用草案的參數語法,例如 1.5rem 符合 <length>,百分比則不屬於這個型別。
這兩個參數刻意保持少量,因為它們對應前面已經確認的差異。圓角仍是共用規則,標題仍跟隨外框的強調色;讀呼叫位置,就能看出這個元件選了哪種變化。
參數雖然透過 var() 讀取,卻不等於在元素上新增一般、公開的自訂屬性。依目前的參數模型,同名參數會遮蔽外部值;它們的私有作用範圍也限制了哪些元素能取得值。上例只處理容器及其子元素;如果改成選取兄弟元素,就不能直接假設參數仍可用。
巢狀的 & > .site-panel-title 讓「外框與標題共用強調色」成為同一份規則。這也建立了一項結構約定:標題必須是容器的直接子元素。若某些元件沒有這個結構,就應該拆開標題與外框,而不是持續擴充選擇器來包容所有情況。
用 @contents 共用條件,把內容留給呼叫者
現在加入另一項需求:畫面變窄時,卡片與提示都要調整,但調整的內容不一樣。卡片需要更緊密的排列,提示則要縮小內距、加強側邊框。
如果這是整個頁面共同採用的窄版條件,可以把條件命名,透過 @contents 留出放入樣式的位置。以下草案範例接在上一節的 CSS 後面:
/* 草案語法:共用條件,各自提供內容 */
@mixin --compact-layout() {
@media (width <= 40rem) {
@contents;
}
}
.site-card {
@apply --compact-layout() {
display: grid;
gap: 0.75rem;
padding: 1rem;
}
}
.site-notice {
@apply --compact-layout() {
padding: 0.75rem;
border-inline-start-width: 3px;
}
}@apply 後面的區塊是呼叫者交進來的內容,@contents 是放入該內容的位置。草案的 @contents 定義允許 mixin 接受這種區塊,因此共用的是「何時進入窄版」,而各自如何改變,仍留在卡片與提示的樣式裡。
40rem 是這個假設案例選定的版面條件,不是通用的裝置分類。若卡片與提示應該依不同容器的可用空間調整,硬共用同一個 viewport 門檻就會失去意義。此時應重新檢查條件,而不是因為已經有 mixin 就繼續使用。
@contents 也可以提供預設區塊:只有呼叫者完全沒傳內容時才使用;傳入空區塊則表示有傳內容,只是內容為空。本文的 --compact-layout() 沒有預設內容,所以只呼叫它、卻不提供區塊,不會替元件增加任何宣告。
對目前這兩個元件,直接寫一個 @media 並在裡面列出兩條規則,依然很容易理解。條件 mixin 的價值,要在多處確實共享同一個版面判斷、又希望樣式留在各自元件附近時才會出現。多一個名稱,應該換來更清楚的意圖。
抽象的成本,會出現在修改的那一天
假設接下來又增加促銷卡片、錯誤提示與側欄摘要。你可能想替 --panel-frame() 加上邊框粗細、背景、圖示位置、標題大小,再補幾個開關控制特殊情況。
參數越來越多時,值得重新檢查的是它們的關係。如果多數呼叫都要覆寫一半設定,或只有某個元件使用一組特殊參數,原本的「共用外框」可能已經變成數個需求的集合。此時,把規則拆小,甚至讓某些元件保留自己的宣告,都可能更好維護。
我會用三個問題檢查這個界線:
- 修改會一起發生嗎? 圓角調整應該影響所有容器,還是只影響某一種內容?
- 呼叫位置能說明意圖嗎? 顏色與內距容易理解,一長串位置引數則需要反覆查定義。
- 例外放在哪裡最清楚? 只屬於提示區塊的側邊框,可以留在提示的規則中,不必升格為全站共用的選項。
抽成 mixin 後,維護者至少需要連起呼叫位置、定義與傳入的值。若 mixin 裡又呼叫其他 mixin,追查路徑還會變長。集中修改帶來的便利,必須和這個閱讀成本一起考慮。
混入的宣告也仍然需要與其他樣式共同運作;套用了 mixin,不代表原本的 cascade 問題就消失。例如在這個簡單案例中,可以把元件自己的圓角宣告放在 @apply 後面,表達局部差異;若另一個來源又設定同一屬性,仍要查看來源、layer、選擇器權重與順序。抽象不應讓實際生效的值更難解釋。
更重要的是,class 與 mixin 都會建立共同修改的關係。兩處今天長得相似,並不足以證明它們永遠應該同步。先替規則說出一個合理的名字,再選擇實作工具,往往比先追求最少行數可靠。
現在值得學的是哪一部分?
如果你熟悉 Sass,這個概念不陌生。不過,Sass 的 mixin使用 @include 套用、以 $ 命名參數,由 Sass 處理後輸出 CSS;本文的原生提案使用 @apply、-- 名稱與 CSS 的值系統。理解用途上的相似,不能直接推導出語法、作用範圍或遷移方式相同。
同一份 CSS 草案中的 @function則處理值:例如依輸入計算一個長度,供 padding 等屬性使用。@mixin 處理的是樣式區塊,可以包含多項宣告與巢狀規則。要重用的東西停在哪一層,會影響應該選擇哪一種能力。
草案本身也仍在整理。本文依據第 5.1 至 5.4 節的定義撰寫範例;第 5.5 節仍有待配合新私有屬性模型更新的註記,並保留舊 @result 寫法。因此,這篇文章沒有把內部替換步驟當成已定案的行為,也沒有把語法解說當作相容性保證。
回到一開始的文件網站,我仍會先採用共用 class 或選擇器分組,讓自訂屬性承擔明確的值差異。等規則真的需要跨元件組合、需要一個清楚的參數介面,再評估 mixin 的表達是否值得。
原生 @mixin 提案提供了一個值得理解的方向:CSS 的重用可以從單一值,延伸到有名字、可調整的一組規則。要讓這個能力帶來好處,我們仍得先回答那個最初的問題:下一次修改時,究竟有哪些樣式應該一起改變?
