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

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

David Darnes
David Darnes

我是 Dave,是 Nordhealth 的前端資深開發人員。我負責設計系統 Nord 的設計及開發工作,其中包括打造元件庫適用的 Web 元件。我想與您分享如何運用 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 編寫的 Web 元件。

但「網頁元件」最棒的一點就是可與幾乎任何現有的 JavaScript 架構搭配使用,甚至完全沒有任何架構。一旦網頁中參照了主要 JavaScript 套件,使用網頁元件就會非常類似使用原生 HTML 元素。唯一的明顯特徵是標記中一致的連字號,這是向瀏覽器表示這是網頁元件的標準。


// TODO: DevSite - Code sample removed as it used inline event handlers
使用網頁上建立的網頁元件。

Shadow DOM 樣式封裝

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

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

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

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


: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);
}
Google 的 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>
使用網頁上的分頁群組元件,並將邊框間距自訂屬性更新為更大的尺寸。

上述範例為我們的其中一個網頁元件,其中含有情境式自訂屬性透過選取器變更。這整個方法的結果是元件,可為使用者提供足夠的樣式彈性,同時仍保有大部分實際樣式。此外,元件開發人員還能攔截使用者套用的樣式。如果想要調整或擴充其中一項屬性,使用者不必變更任何程式碼,

我們發現這個做法非常強大,不僅能造福我們設計系統元件的建立者,對開發團隊而言,也在產品中使用這些元件。

進一步使用自訂屬性

在撰寫本文時,我們並未在說明文件中揭露這些內容相關的自訂屬性,但我們計畫將這些屬性公開,讓更多開發團隊能夠瞭解並運用這些屬性。我們的元件會封裝在 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

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