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

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

David Darnes
David Darnes

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

為了建構我們的 Web 元件,我們使用 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 元件最吸引人的地方,是幾乎可與任何現有的 JavaScript 架構搭配使用,甚至完全不需架構。一旦網頁中參照了主要的 JavaScript 套件,使用網頁元件就會非常類似使用原生 HTML 元素。唯一的明顯特徵是標記中一致的連字號,這是向瀏覽器表明這是網頁元件的標準。

Shadow DOM 樣式封裝

就像原生 HTML 元素有 Shadow DOM 一樣,Web 元件也有。Shadow DOM 是元素內部隱藏的節點樹狀結構。如要將這項功能視覺化,最佳做法是開啟網頁檢查器,然後開啟「Show Shadow DOM tree」選項。完成後,請嘗試在檢查器中查看原生輸入元素,您現在可以選擇開啟該輸入內容,並查看其中的所有元素。您甚至可以試著在其中一個網頁元件上進行這項操作,試著檢查自訂輸入元件的 Shadow DOM。

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

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

這種樣式封裝方式是元件程式庫的優點。這樣一來,無論父項網頁套用的樣式為何,使用者在使用我們的元件時,都能看到我們預期的樣貌,這點我們更有把握。為進一步確保,我們會將 all: unset; 新增至所有 Web 元件的根目錄或「主機」。


:host {
  all: unset;
  display: block;
  box-sizing: border-box;
  text-align: start;
  /* ... */
}
部分元件範本程式碼會套用至陰影根或主機選取器。

不過,如果使用您 Web 元件的使用者有正當理由變更特定樣式,該怎麼辦呢?也許有一段文字因內容而需要更強的對比度,或者邊框需要加粗?如果元件無法套用任何樣式,您該如何解鎖這些樣式選項?

這時,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 架構將自訂屬性套用至根元素,大量使用設計符記的自訂屬性,也就是說,無論是 Web 元件、CSS 輔助類別,還是開發人員想要從符記清單中擷取值,網頁上的所有元素都可以使用這些符記值。

透過使用 var() 函式,我們可以繼承自訂屬性,並穿透 Web 元件的 Shadow DOM,讓開發人員在設定元件樣式時,享有更精細的控制權。

Nord Web 元件中的自訂屬性

無論我們為設計系統開發哪個元件,都會針對 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,供該元件的使用者使用。


<nord-tab-group label="Title">
  <!-- ... -->
</nord-tab-group>

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

上例顯示其中一個 Web 元件,其中的內容相關自訂屬性已透過選取器進行變更。這個整體做法所產生的結果,就是一個可為使用者提供足夠樣式彈性,同時仍可控管大部分實際樣式的元件。此外,我們身為元件開發人員,還可以攔截使用者套用的樣式。如果我們想調整或擴充其中一個屬性,使用者不必變更任何程式碼。

我們發現這種做法非常實用,不只對設計系統元件的開發者 (也就是我們) 有幫助,也能協助開發團隊在產品中使用這些元件。

進一步使用自訂屬性

在撰寫本文時,我們並未在說明文件中揭露這些內容相關的自訂屬性,但我們計畫將這些屬性公開,讓更多開發團隊能夠瞭解並善用這些屬性。我們的元件會透過資訊清單檔案在 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);
  /* ... */
}
分隔符 Web 元件 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 元件。歡迎與我們分享你的想法,如果你決定在自己的作品中使用其中任何一種方法,歡迎在 Twitter 上找我 @DavidDarnes。您也可以在 Twitter 上找到 Nordhealth 的 @NordhealthHQ,以及我的團隊成員:@Viljamis@WickyNilliams@eric_habich,他們都致力於整合這套設計系統,並實作本文提及的功能。

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