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

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

David Darnes
David Darnes

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

我們如何建構網頁元件

我們使用 Lit 建構我們的網頁元件。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 套件後,使用 Web 元件的方法就和使用原生 HTML 元素非常類似。唯一有效的警示標誌是,它不是原生 HTML 元素,就是代碼內一致的連字號,這是向瀏覽器表明這是網頁元件的標準。


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

陰影 DOM 樣式封裝

就像原生 HTML 元素具有 Shadow DOM 一樣,Web 元件也是如此。陰影 DOM 是元素中節點的隱藏樹狀結構。視覺化呈現相關數據的最佳方式,就是開啟網頁檢查器,並開啟「顯示陰影 DOM 樹狀結構」選項。完成這項作業後,請嘗試在檢查器中查看原生輸入元素,您現在就可以選擇開啟該輸入項目,並查看其中的所有元素。您甚至可以使用我們的其中一個網頁元件嘗試這項功能,請嘗試檢查我們的自訂輸入元件,查看其 Shadow DOM。

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

使用 Shadow DOM 的其中一項優點 (或缺點,視你的前景而定) 就是樣式封裝。當您在網頁元件中編寫 CSS 時,這些樣式就無法外洩並影響主頁面或其他元素,而是完全包含在元件中。此外,為主頁面或上層 Web 元件編寫的 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 架構將自訂屬性套用到根元素,藉此大量將自訂屬性用於設計權杖。也就是說,網頁上的所有元素皆可使用這些權杖值,無論是 Web 元件、CSS 輔助類別,還是開發人員,都能從權杖清單中擷取值。

運用 var() 函式沿用自訂屬性的功能,就是我們運用網頁元件的 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,而該元件的使用者只要使用該 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);
  /* ... */
}
調整了符合情境自訂屬性的分隔線 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 自訂屬性的網頁元件。歡迎與我們分享你的看法。如果您決定使用上述任一種方法,歡迎透過 Twitter (@DavidDarnes) 與我們聯繫。您也可以在 Twitter 中找到 Nordhealth @NordhealthHQ,以及我們的其他團隊成員,他們負責整合這個設計系統並執行以下文章中提及的功能:@Viljamis@WickyNilliams@eric_habich

主頁橫幅:Dan Cristian Păduresee