デザイン システムとコンポーネント ライブラリでカスタム プロパティを使用するメリット。
Nordhealth でシニア フロントエンド デベロッパーを務める Dave です。私は、デザイン システム Nord の設計と開発に取り組んでいます。これには、コンポーネント ライブラリ用の Web コンポーネントの構築も含まれます。CSS カスタム プロパティを使用して Web Components のスタイリングに関する問題を解決した方法と、デザイン システムやコンポーネント ライブラリでカスタム プロパティを使用するその他のメリットについてご紹介します。
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);
しかし、Web コンポーネントの最も魅力的な点は、既存のほとんどの JavaScript フレームワーク、またはフレームワークなしでも動作することです。メインの JavaScript パッケージがページで参照されると、Web コンポーネントの使用はネイティブの HTML 要素の使用とほぼ同じになります。ネイティブの HTML 要素ではないことを示す唯一の目印は、タグ内のハイフンです。これは、ブラウザに Web コンポーネントであることを示すための標準です。
Shadow DOM スタイルのカプセル化
ネイティブの HTML 要素に Shadow DOM があるのと同様に、Web Components にもあります。Shadow DOM は、要素内のノードの非表示ツリーです。これを視覚化する最善の方法は、ウェブ インスペクタを開いて [Shadow DOM ツリーを表示] オプションをオンにすることです。この操作を行うと、インスペクタでネイティブの入力要素を確認できるようになります。入力要素を開いて、その中のすべての要素を確認できるようになります。この方法を Web Components で試すこともできます。カスタム入力コンポーネントを検査して、その Shadow DOM を確認してみてください。
Shadow DOM の利点(または欠点、見方によって異なります)の 1 つは、スタイルのカプセル化です。ウェブ コンポーネント内で CSS を記述すると、そのスタイルが漏れてメインページや他の要素に影響を与えることはありません。スタイルはコンポーネント内に完全に含まれます。また、メインページや親のウェブ コンポーネント用に記述された CSS がウェブ コンポーネントに漏れることはありません。
このスタイルのカプセル化は、コンポーネント ライブラリのメリットです。これにより、親ページに適用されたスタイルに関係なく、コンポーネントが意図したとおりに表示されることが保証されます。さらに、すべてのウェブ コンポーネントのルート(ホスト)に all: unset; を追加します。
:host {
  all: unset;
  display: block;
  box-sizing: border-box;
  text-align: start;
  /* ... */
}
ただし、Web コンポーネントを使用しているユーザーが、特定のスタイルを変更する正当な理由がある場合はどうすればよいでしょうか。コンテキストによっては、テキストのコントラストを上げる必要がある場合や、枠線を太くする必要がある場合もあります。コンポーネントにスタイルが適用されない場合、スタイリング オプションをどのようにして有効にすればよいでしょうか?
そこで CSS カスタム プロパティの出番です。
CSS カスタム プロパティ
カスタム プロパティは、その名のとおり、自分で名前を付けて必要な値を適用できる CSS プロパティです。ただし、2 つのハイフンを接頭辞として付ける必要があります。カスタム プロパティを宣言すると、var() 関数を使用して CSS で値を使用できるようになります。
:root {
  --n-color-accent: rgb(53, 89, 199);
  /* ... */
}
.n-color-accent-text {
  color: var(--n-color-accent);
}
継承に関しては、すべてのカスタム プロパティが継承されます。これは、通常の CSS プロパティと値の一般的な動作に沿ったものです。親要素または要素自体に適用されたカスタム プロパティは、他のプロパティの値として使用できます。Google では、CSS フレームワークを介してルート要素に適用することで、デザイン トークンにカスタム プロパティを多用しています。つまり、ウェブ コンポーネント、CSS ヘルパークラス、トークンリストから値を取得したいデベロッパーなど、ページ上のすべての要素がこれらのトークン値を使用できます。
var() 関数を使用してカスタム プロパティを継承する機能により、Web コンポーネントの Shadow DOM を貫通し、デベロッパーがコンポーネントのスタイル設定をより細かく制御できるようになります。
Nord ウェブ コンポーネントのカスタム プロパティ
デザイン システムのコンポーネントを開発する際は、CSS に慎重に取り組んでいます。Google は、無駄がなく、保守しやすいコードを目指しています。デザイン トークンは、ルート要素のメイン 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 プロパティに直接値を適用する場合もあれば、新しいコンテキストのカスタム プロパティを定義して、そのプロパティに値を適用する場合もあります。
: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);
  /* ... */
}
また、コンポーネントに固有でトークンに含まれていない値の一部を抽象化し、コンテキスト カスタム プロパティに変換します。コンポーネントのコンテキストに沿ったカスタム プロパティには、次の 2 つの大きなメリットがあります。まず、この値はコンポーネント内の複数のプロパティに適用できるため、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);
}
2 つ目は、コンポーネントの状態とバリエーションの変更が非常にクリーンになることです。たとえば、ホバー状態やアクティブ状態、またはこの例のバリエーションのスタイルを設定する場合、カスタム プロパティを変更するだけで、すべてのプロパティを更新できます。
:host([padding="l"]) {
  --n-tab-group-padding: var(--n-space-l);
}
しかし、最も強力なメリットは、コンポーネントでこれらのコンテキスト カスタム プロパティを定義すると、各コンポーネントのカスタム CSS 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 コンポーネントの 1 つを示しています。このアプローチの結果、実際のスタイルのほとんどをチェックしながら、ユーザーに十分なスタイリングの柔軟性を提供するコンポーネントが作成されます。さらに、コンポーネント デベロッパーは、ユーザーが適用したスタイルをインターセプトできます。これらのプロパティのいずれかを調整または拡張する場合、ユーザーがコードを変更する必要はありません。
このアプローチは、デザイン システム コンポーネントの作成者である私たちだけでなく、開発チームが製品でこれらのコンポーネントを使用する際にも非常に有効です。
カスタム プロパティの活用
執筆時点では、これらのコンテキスト カスタム プロパティはドキュメントで公開されていませんが、より多くの開発チームがこれらのプロパティを理解して活用できるように、公開する予定です。コンポーネントは、npm でマニフェスト ファイルとともにパッケージ化されています。このファイルには、コンポーネントに関するすべての情報が含まれています。ドキュメント サイトのデプロイ時に、マニフェスト ファイルをデータとして使用します。これは、Eleventy とそのグローバル データ機能を使用して行われます。これらのコンテキスト カスタム プロパティは、このマニフェスト データファイルに含める予定です。
もう 1 つ改善したいのは、コンテキストに応じたカスタム プロパティが値を継承する方法です。たとえば、現在、2 つの区切り線コンポーネントの色を調整するには、セレクタを使用して両方のコンポーネントを明示的にターゲットにするか、スタイル属性を持つ要素にカスタム プロパティを直接適用する必要があります。これでも問題ないように見えますが、デベロッパーがこれらのスタイルをコンテナ要素またはルートレベルで定義できると、より便利になります。
<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);
  /* ... */
}
このようにコンテキスト カスタム プロパティを定義すると、グローバル トークン値の継承やコンポーネント コード全体での値の再利用など、以前と同じことをすべて実行できます。また、コンポーネントは、そのプロパティの新しい定義をコンポーネント自体または親要素で適切に継承します。
<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>
この方法は真に「プライベート」ではないという議論もあるかもしれませんが、私たちが懸念していた問題に対するかなり優れた解決策だと考えています。機会があれば、コンポーネントでこの問題に取り組む予定です。これにより、開発チームはコンポーネントの使用をより細かく制御できるようになり、同時に、Google が導入しているガードレールも活用できます。
CSS カスタム プロパティで Web Components を使用する方法についてのこの分析が、皆様のお役に立てば幸いです。ご意見をお聞かせください。また、これらの方法を実際の作業で試してみる場合は、Twitter の @DavidDarnes で私を見つけることができます。Twitter で Nordhealth(@NordhealthHQ)をフォローすることもできます。また、この記事で紹介したデザイン システムの構築と機能の実装に尽力したチームメンバー(@Viljamis、@WickyNilliams、@eric_habich)もフォローできます。
ヒーロー画像: Dan Cristian Pădureț