Nordhealth 在網頁元件中使用自訂屬性的方式

在設計系統和元件庫中使用自訂屬性的好處。

David Darnes
David Darnes

我是 Dave,是 Nordhealth 的資深前端開發人員。我負責設計及開發設計系統 Nord,包括為元件程式庫建構 Web 元件。我想分享我們如何使用 CSS 自訂屬性,解決 Web Components 的樣式問題,以及在設計系統和元件庫中使用自訂屬性的其他優點。

我們如何建構網頁元件

我們使用 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);
以 Lit 撰寫的網頁元件。

但 Web Components 最吸引人的地方在於,它們幾乎能與任何現有的 JavaScript 架構搭配使用,甚至完全不需要架構。在網頁中參照主要 JavaScript 套件後,使用網頁元件的方式與使用原生 HTML 元素非常相似。判斷是否為原生 HTML 元素的唯一真正指標,就是標記內的一致連字號,這是向瀏覽器表示這是網頁元件的標準。

Shadow DOM 樣式封裝

原生 HTML 元素有 Shadow DOM,網頁元件也有。Shadow DOM 是元素內隱藏的節點樹狀結構。如要以視覺化方式呈現,最好的方法是開啟網頁檢查器,並啟用「顯示 Shadow DOM 樹狀結構」選項。完成後,請嘗試在檢查器中查看原生輸入元素,現在您應該可以開啟該輸入元素,並查看其中的所有元素。您甚至可以試試我們的其中一個網頁元件,例如檢查自訂輸入元件,看看它的 Shadow DOM。

在開發人員工具中檢查的 shadow DOM。
一般文字輸入元素和 Nord 輸入網頁元件中的 Shadow DOM 範例。

Shadow DOM 的優點 (或缺點,視您的觀點而定) 之一是樣式封裝。如果您在網頁元件中編寫 CSS,這些樣式不會外洩,也不會影響主要網頁或其他元素,而是完全包含在元件中。此外,為主要網頁或父項網頁元件編寫的 CSS 無法洩漏至網頁元件。

這種樣式封裝是元件庫的一項優點。這樣一來,無論套用至父項網頁的樣式為何,我們都能更有把握地確保使用者採用元件時,元件的外觀會符合我們的預期。為進一步確保,我們會在所有 Web 元件的根目錄或「主機」中新增 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 架構將這些屬性套用至根元素,這表示網頁上的所有元素都能使用這些符記值,無論是網頁元件、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 自訂屬性。

這些權杖值隨後會在我們的元件中參照。在某些情況下,我們會直接在 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="T>itl<e"
  >!<-- ... --
/nord>-t<ab-gr>oup

style
  nord-tab-group {
    --n-tab-group-padding: var(--n-space<-xl);
>  }
/style
在頁面上使用分頁群組元件,並將邊框間距自訂屬性更新為較大的尺寸。

上例顯示其中一個 Web Components,其情境式自訂屬性已透過選取器變更。這種做法的結果是,元件可為使用者提供足夠的樣式彈性,同時仍能控管大部分的實際樣式。此外,我們身為元件開發人員,還能攔截使用者套用的樣式。如果我們想調整或延長其中一項屬性,使用者不必變更任何程式碼,即可完成這項操作。

我們發現這種做法非常強大,不僅對我們這些設計系統元件的創作者來說是如此,對開發團隊在產品中使用這些元件時也是如此。

進一步瞭解自訂屬性

撰寫本文時,我們實際上並未在文件中揭露這些情境式自訂屬性,但我們計畫這麼做,讓更廣泛的開發團隊瞭解及運用這些屬性。我們的元件會透過 npm 封裝,並附上資訊清單檔案,其中包含元件的所有相關資訊。接著,我們會在部署說明文件網站時,使用 Eleventy 及其 Global Data 功能,將資訊清單檔案當做資料使用。我們計畫將這些情境式自訂屬性納入這個資訊清單資料檔案。

我們希望改善的另一個領域,是這些情境式自訂屬性如何繼承值。舉例來說,如要調整兩個分隔線元件的顏色,目前必須使用選取器指定這兩個元件,或直接在具有樣式屬性的元素上套用自訂屬性。這似乎沒什麼問題,但如果開發人員可以在包含元素或甚至根層級定義這些樣式,會更有幫助。


<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);
  /* ... */
}
調整分隔線網頁元件 CSS,並搭配情境自訂屬性,讓內部 CSS 依附於已設為具有備援的公開自訂屬性的私有自訂屬性。

以這種方式定義情境式自訂屬性,表示我們仍可執行先前所有作業,例如繼承全域權杖值,以及在元件程式碼中重複使用值;但元件也會順利繼承自身或任何父項元素上該屬性的新定義。


<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 自訂屬性和 Web Components 的文章很有幫助。歡迎分享你的想法。如果你決定在自己的工作中使用這些方法,歡迎在 Twitter 上 @DavidDarnes 與我聯絡。你也可以在 Twitter 上找到 Nordhealth @NordhealthHQ,以及我的團隊成員,他們努力整合這個設計系統,並執行本文提及的功能:@Viljamis@WickyNilliams@eric_habich

主頁橫幅圖片由 Dan Cristian Pădureț 提供