Nordhealth 如何在網頁元件中使用自訂屬性

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

David Darnes
David Darnes

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

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

Shadow DOM 樣式封裝

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

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

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

這種樣式封裝是元件庫的一大優點。這樣一來,無論父項頁面套用何種樣式,我們都能確保使用者看到的元件符合預期。為進一步確保這一點,我們在所有網頁元件的根目錄或「主機」中加入 all: unset;。


:host {
  all: unset;
  display: block;
  box-sizing: border-box;
  text-align: start;
  /* ... */
}
套用至 Shadow Root 或主機選取器的部分元件樣板程式碼。

不過,如果使用網頁元件的使用者有正當理由變更特定樣式,該怎麼辦?也許某行文字需要根據背景提高對比度,或邊框需要加粗?如果沒有任何樣式可以進入元件,該如何解鎖這些樣式選項?

這時 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);
  /* ... */
}
在元件的 Shadow Root 上定義自訂屬性,然後在元件樣式中使用。此外,系統也會使用設計權杖清單中的自訂屬性。

我們也會擷取元件專屬但不在權杖中的值,並將其轉換為情境自訂屬性。與元件相關的自訂屬性有兩大好處。首先,這表示我們可以更「精簡」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);
  /* ... */
}
分隔線網頁元件 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 自訂屬性的文章實用。歡迎與我們分享你的想法。如果你決定在自己的工作中使用上述任何方法,可以透過 Twitter @DavidDarnes 與我聯絡。您也可以在 Twitter 上找到 Nordhealth @NordhealthHQ,以及我的其他團隊成員,他們努力整合這個設計系統,並執行本文件中提到的功能:@Viljamis、@WickyNilliams 和 @eric_habich。