在設計系統和元件庫中使用自訂屬性的好處。
我是 Dave,在 Nordhealth 擔任資深前端開發人員。我負責設計和開發設計系統 Nord,包括為元件庫建構網頁元件。我想分享我們如何使用 CSS 自訂屬性解決網頁元件樣式問題,以及在設計系統和元件庫中使用自訂屬性的其他好處。
我們如何建構網頁元件
為了建構網頁元件,我們使用 Lit,這個程式庫提供許多樣板程式碼,例如狀態、限定範圍的樣式、範本等。Lit 不僅輕量,而且是以原生 JavaScript API 建構而成,這表示我們可以提供精簡的程式碼套件,充分利用瀏覽器既有的功能。
import {html, css, LitElement} from 'lit';
export class SimpleGreeting extends LitElement {
static styles = css`:host { color: blue; font-family: sans-serif; }`;
static properties = {
name: {type: String},
};
constructor() {
super();
this.name = 'there';
}
render() {
return html`Hey
${this.name}, welcome to Web Components!`;
}
}
customElements.define('simple-greeting', SimpleGreeting);
不過,網頁元件最吸引人的地方在於,它們幾乎能與任何現有的 JavaScript 架構搭配使用,甚至完全不需要架構。在網頁中參照主要 JavaScript 套件後,使用網頁元件的方式與使用原生 HTML 元素非常相似。唯一能辨別這不是原生 HTML 元素的跡象,是標記中一致的連字號,這是向瀏覽器表示這是網頁元件的標準。
Shadow DOM 樣式封裝
與原生 HTML 元素類似,網頁元件也有 Shadow DOM。Shadow DOM 是元素內隱藏的節點樹狀結構。如要以視覺化方式呈現,最好的方法是開啟網頁檢查器,並啟用「顯示 Shadow DOM 樹狀結構」選項。完成後,請嘗試在檢查器中查看原生輸入元素,現在您可以選擇開啟該輸入元素,並查看其中的所有元素。您甚至可以嘗試使用我們的其中一個網頁元件,檢查自訂輸入元件,查看其 Shadow DOM。
Shadow DOM 的優點 (或缺點,視您的觀點而定) 之一是樣式封裝。如果您在網頁元件中編寫 CSS,這些樣式不會外洩並影響主頁面或其他元素,而是完全包含在元件中。此外,為主網頁或父項網頁元件編寫的 CSS 不會洩漏到網頁元件中。
這種樣式封裝是元件庫的一大優點。這樣一來,無論父項頁面套用何種樣式,我們都能確保使用者看到的元件符合預期。為進一步確保這一點,我們在所有網頁元件的根目錄或「主機」中加入 all: unset;。
:host {
all: unset;
display: block;
box-sizing: border-box;
text-align: start;
/* ... */
}
不過,如果使用網頁元件的使用者有正當理由變更特定樣式,該怎麼辦?也許某行文字需要根據背景提高對比度,或邊框需要加粗?如果沒有任何樣式可以進入元件,該如何解鎖這些樣式選項?
這時 CSS 自訂屬性就能派上用場。
CSS 自訂屬性
自訂屬性的名稱非常貼切,你可以完全自行命名 CSS 屬性,並套用任何所需的值。唯一的要求是,您必須在變數名稱前加上兩個連字號。宣告自訂屬性後,您可以使用 var() 函式在 CSS 中使用該值。
:root {
--n-color-accent: rgb(53, 89, 199);
/* ... */
}
.n-color-accent-text {
color: var(--n-color-accent);
}
所有自訂屬性都會繼承,這符合一般 CSS 屬性和值的典型行為。套用至父項元素或元素本身的任何自訂屬性,都可以做為其他屬性的值。我們大量使用自訂屬性來定義設計權杖,方法是使用 CSS 架構將這些屬性套用至根元素,這表示頁面上的所有元素都可以使用這些權杖值,無論是網頁元件、CSS 輔助類別,還是想要從權杖清單中擷取值的開發人員。
這種繼承自訂屬性的能力,搭配 var() 函式,可讓我們穿透 Web 元件的 Shadow DOM,讓開發人員在設定元件樣式時,能更精細地控管。
Nord 網頁元件中的自訂屬性
每當我們為設計系統開發元件時,都會仔細考量 CSS,目標是精簡且易於維護的程式碼。我們現有的設計權杖,是在根元素的主要 CSS 架構中定義為自訂屬性。
:root {
--n-space-m: 16px;
--n-space-l: 24px;
/* ... */
--n-color-background: rgb(255, 255, 255);
--n-color-border: rgb(216, 222, 228);
/* ... */
}
接著,我們的元件會參照這些權杖值。在某些情況下,我們會直接在 CSS 屬性上套用值,但在其他情況下,我們會實際定義新的情境自訂屬性,並將值套用至該屬性。
:host {
--n-tab-group-padding: 0;
--n-tab-list-background: var(--n-color-background);
--n-tab-list-border: inset 0 -1px 0 0 var(--n-color-border);
/* ... */
}
.n-tab-group-list {
box-shadow: var(--n-tab-list-border);
background-color: var(--n-tab-list-background);
gap: var(--n-space-s);
/* ... */
}
我們也會擷取元件專屬但不在權杖中的值,並將其轉換為情境自訂屬性。與元件相關的自訂屬性有兩大好處。首先,這表示我們可以更「精簡」CSS,因為該值可以套用至元件內的多個屬性。
.n-tab-group-list::before {
/* ... */
padding-inline-start: var(--n-tab-group-padding);
}
.n-tab-group-list::after {
/* ... */
padding-inline-end: var(--n-tab-group-padding);
}
第二,這能讓元件狀態和變化版本變更變得非常乾淨,只需要變更自訂屬性,就能更新所有屬性,例如設定懸停或啟用狀態的樣式,或是在本例中設定變化版本。
:host([padding="l"]) {
--n-tab-group-padding: var(--n-space-l);
}
不過,最強大的優點是,當我們在元件上定義這些情境自訂屬性時,會為每個元件建立一種自訂 CSS API,元件的使用者可以利用這個 API。
<nord-tab-group label="Title">
<!-- ... -->
</nord-tab-group>
<style>
nord-tab-group {
--n-tab-group-padding: var(--n-space-xl);
}
</style>
上例顯示其中一個網頁元件,其情境自訂屬性已使用選取器變更。這種做法的結果是,元件可為使用者提供足夠的樣式彈性,同時仍能控制大部分的實際樣式。此外,身為元件開發人員,我們還能攔截使用者套用的樣式。如果我們想調整或擴充其中一項屬性,使用者不必變更任何程式碼。
我們發現這種方法非常有效,不僅對我們這些設計系統元件的創作者來說是如此,對在產品中使用這些元件的開發團隊來說也是如此。
進一步瞭解自訂屬性
撰寫本文時,我們並未在說明文件中揭露這些情境自訂屬性,但我們計劃這麼做,讓更廣泛的開發團隊瞭解並運用這些屬性。我們的元件會與資訊清單檔案一起封裝在 npm 上,其中包含所有相關資訊。接著,我們在部署說明文件網站時,使用 Eleventy 及其全域資料功能,將資訊清單檔案當做資料使用。我們計劃將這些情境自訂屬性納入這個資訊清單資料檔案。
我們也想改善這些情境自訂屬性繼承值的方式。舉例來說,如果現在想調整兩個分隔線元件的顏色,就必須使用選取器特別指定這兩個元件,或使用樣式屬性直接在元素上套用自訂屬性。這看起來可能沒問題,但如果開發人員可以在包含元素或甚至根層級定義這些樣式,會更有幫助。
<nord-divider></nord-divider>
<section>
<nord-divider></nord-divider>
<!-- ... -->
</section>
<style>
nord-divider {
--n-divider-color: var(--n-color-status-danger);
}
section {
padding: var(--n-space-s);
background: var(--n-color-surface-raised);
}
section nord-divider {
--n-divider-color: var(--n-color-status-success);
}
</style>
之所以必須直接在元件上設定自訂屬性值,是因為我們使用元件主機選取器,在同一個元素上定義這些屬性。我們直接在元件中使用的全域設計權杖會直接傳遞,不受這個問題影響,甚至可以在父項元素上攔截。如何兼顧兩者?
私人和公開自訂屬性
私人自訂屬性是由 Lea Verou 整合而成,這是元件本身的內容相關「私人」自訂屬性,但設為具有備用值的「公開」自訂屬性。
:host {
--_n-divider-color: var(--n-divider-color, var(--n-color-border));
--_n-divider-size: var(--n-divider-size, 1px);
}
.n-divider {
border-block-start: solid var(--_n-divider-size) var(--_n-divider-color);
/* ... */
}
以這種方式定義情境自訂屬性,代表我們仍可執行先前執行的所有操作,例如沿用全域權杖值,以及在整個元件程式碼中重複使用值;但元件也會順利沿用該屬性本身或任何父項元素的新定義。
<nord-divider></nord-divider>
<section>
<nord-divider></nord-divider>
<!-- ... -->
</section>
<style>
nord-divider {
--n-divider-color: var(--n-color-status-danger);
}
section {
padding: var(--n-space-s);
background: var(--n-color-surface-raised);
--n-divider-color: var(--n-color-status-success);
}
</style>
雖然有人認為這種方法並非真正的「私密」,但我們仍認為這是解決我們擔憂問題的絕佳方案。有機會的話,我們會著手處理元件中的這個問題,讓開發團隊能進一步控管元件的使用方式,同時仍能享有現有的防護措施。
希望您覺得這篇深入探討如何搭配使用網頁元件和 CSS 自訂屬性的文章實用。歡迎與我們分享你的想法。如果你決定在自己的工作中使用上述任何方法,可以透過 Twitter @DavidDarnes 與我聯絡。您也可以在 Twitter 上找到 Nordhealth @NordhealthHQ,以及我的其他團隊成員,他們努力整合這個設計系統,並執行本文件中提到的功能:@Viljamis、@WickyNilliams 和 @eric_habich。