AI × Stencil.jsで進めるデザインシステム実装 ― コンポーネント設計の難所と配布まで

AI × Stencil.jsで進めるデザインシステム実装 コンポーネント設計の難所と配布まで

前編では、デザインシステムの考え方から、Stencil.js のセットアップ、Tokens Studio(Figma Tokens) の取り込み、AIを使ったコンポーネント作成の入り口までを整理しました。

後編ではその続きとして、実際に実装を進める中で詰まりやすかったポイントと、完成前に見ておきたい品質チェック、そして利用側へ配布するところまでをまとめます。

この記事をIDEに読み込ませるコンテキストとして使うのもおすすめです。

コンポーネント構築で一番苦労したこと(と避け方)

たたき台を作るところまでは速いです。 難しいのは、コンポーネントをアクセシブルで、再利用可能かつ保守しやすい状態に保つための、アーキテクチャ設計の判断を下すことです。

ネイティブ要素はできるだけそのまま使う

最初にぶつかる設計判断のひとつが、ネイティブHTML要素をラップして活かすか、それとも独自実装で置き換えるかです。

<button><input><select><form> には、アクセシビリティ、キーボード操作、フォーカス管理、フォーム参加といった振る舞いが標準で備わっています。
これを自前で作り直すと、実装コストもバグのリスクも一気に増えます。

多くの場合は、ネイティブ要素を置き換えるより、その上に薄く機能を足すほうが安全です。独自実装に寄せるほど、アクセシビリティ要件の管理、disabled の扱い、フォーム送信時の挙動、細かなインタラクションまで自分で面倒を見る必要があります。まずネイティブを活かす方針にしておくと、見落としや長期的な保守負荷を減らせます。

たとえば、ボタンコンポーネントは基本的に実際の <button> を内部で使うべきです。そうすれば、キーボード操作、フォーカス管理、disabled 状態の扱いを標準の挙動としてそのまま利用できます。

リンクとして使うケースなら、無理にボタンで表現するのではなく、<a> を使うほうが自然です。

テキストフィールドも同様で、独自の汎用要素を一から組み立てるのではなく、<input><textarea> を包む形にするのが基本です。
ラベルとの関連付け、フォーム送信、バリデーション、オートコンプリート、モバイルでのキーボード表示、ブラウザのアクセシビリティ支援をそのまま維持できます。

アコーディオンも同じです。見た目全体はカスタムでも、開閉トリガーはクリック可能な <div> ではなく、実際の <button> にしておくべきです。
デザイン要件が合うなら、<details><summary> を使うのも有効な選択肢です。

Shadow DOM:オンにするとき(とオンにしないとき)

Shadow DOMを使うかどうかは、「ネイティブ要素を使うかどうか」とは別の判断です。ここで決めているのは、コンポーネントをどれだけ外側から隔離するかです。

shadow: true にすると、内部のDOMとスタイルをグローバルCSSから切り離せるので、他チームのCSS干渉を受けにくくなります。

一方で、隔離が強くなる分、外側からの調整もしづらくなる場面もあります。

  • グローバルテーマを当てづらい
  • ユーティリティクラスをそのまま効かせにくい
  • slot周りのスタイリングが難しくなる
  • 既存のレイアウトシステムと合わせにくくなる

そのため、外部スタイルとの衝突を防ぎたい自己完結型のコンポーネントには、Shadow DOMが有効です。

逆に、アプリ側で柔軟に見た目を調整したいコンポーネント、既存のCSS設計やトークン運用になじませたいコンポーネントは、Light DOMのほうが扱いやすいことが多いです。

ボタンや入力欄のように、ネイティブ要素を活かしつつ周囲のレイアウトやテーマに自然になじませたい部品は、最初から shadow: false を選ぶほうが素直なこともあります。

SVGの色付け

よくある問題が、アイコンを再利用しつつ正しいトークンカラーを継承させたいケースです。

一番簡単なアプローチは、SVGに直接色を埋め込むのではなく、-webkit-mask(またはCSSマスキング)を使うことです。
そうすれば必要なSVGアセットは1つだけで、デザイントークンの色をCSSで適用できます。

想定外のslot周りの余白

slotは、スロットされたコンテンツとそのラッパーの両方がスペーシングを生み出す可能性があるため、レイアウト上の問題を起こすことがあります。
slotがオプショナルな場合、コンテンツがないときには完全に潰れるようにしましょう。

つまり、slot自体と周囲のコンテナの両方をチェックして、空のギャップ、パディング、マージンが残らないようにする必要があります。

深くネストされたslotコンテンツのスタイリング

コンポーネントの一部にスタイルを当てても何も変わらない場合、深くスロットされた要素をターゲットにしているのが原因かもしれません。

たとえば、大きなナビゲーションコンポーネントに渡されたナビゲーションアイテムは、複数のslotレイヤーを通過している可能性があります。

Shadow DOMの設定では、スロットされたコンテンツは親コンポーネントの内部ツリーの外に存在するため、内部要素と同じ方法でスタイルを当てられないことがあります。

マルチレイヤーのslotパターンはコントロールが難しく、混乱するスタイリングの問題が起きやすくなります。

より良いパターンは、大きなUIパーツを小さな内部コンポーネントに分割して、直接スタイルを当てることです。

たとえば、大量にスロットされた子要素を持つ巨大なナビゲーションコンポーネントを1つ作るのではなく、親のナビゲーションコンテナと別々のナビゲーションアイテムコンポーネントを作ります。

こうすると、構造が明確になり、スタイリングが予測しやすくなり、slot関連の制限も減ります。

コンポーネントの動作チェックを自動化する(テンプレートあり)

コンポーネントがひと通り動くようになったら、「完成」にする前に一度レビューをかけておくのが一番無難です。デザインシステムのミスが複数のコンポーネントに広がる前に拾いやすいタイミングでもあります。

以下のレビューテンプレートは、Stencil.jsで作るコンポーネント設計のUdemyコースを見たあとに、自分用にまとめたものです。もっと丁寧な解説が欲しいなら、このコースも参考になります:

参考:Web Components & Design System with StencilJS
https://www.udemy.com/share/10ediT3@-6fzEwbdXpw4Rb3DObGLltoxmiq7IJ1LCriVk_JuJatkKqIIKPH0cm2kg1mTBgze

すぐに使えるシンプル版はこちら:

Stencilコンポーネントのチェック用プロンプト(IDE AI向け)

対象範囲: src/components/** と関連するデモだけをチェック。
FAIL = 出荷前修正必須、WARN = 保守性リスク。すべての指摘に file:line・理由・最小修正案を付ける。

1. コンポーネント定義
* FAIL: @Component デコレーターがない
* FAIL: タグが kebab-case でない、またはダッシュを含んでいない
* WARN: 同じフォルダに責務の異なるコンポーネントが混在している

2. パブリック API
* FAIL: 外部入力が @Prop ではなく @State に格納されている
* FAIL: mutable でない @Prop が直接再代入されている
* FAIL: 配列・オブジェクトの @State を .push() 等で直接変異させている(変更検知が発火しない)
* WARN: @Prop が関数を受け取っている(@Event 設計が適切か確認)
* WARN: コンポーネントが fetch() やリモートデータ呼び出しを行っている
* WARN: props がプリミティブ値ではなく大きなオブジェクト設定を要求している

3. マークアップ & セマンティクス
* FAIL: ネイティブセマンティック要素が <div> 相当で置き換えられている
* FAIL: インタラクティブなコンポーネントに適切なセマンティクス/キーボード操作がない
* FAIL: アクセシビリティに影響する表示状態(disabled・error・selected など)が ARIA や DOM 属性に反映されず CSS のみで制御されている
* WARN: ネイティブ要素で実現できる箇所を不必要に独自実装している

4. コンポジション
* WARN: 構造的なコンテンツが slot ではなく大きな config オブジェクト経由で渡されている
* WARN: デフォルト slot のほうが適切な場所でシンプルなテキスト prop が使われている

5. スタイリング
* FAIL: スタイルが実行時に手動で注入されている(render() 内 <style> 生成など)
* WARN: トークンが存在するにもかかわらずハードコードされたデザイン値が使われている
* WARN: フォーム関連コンポーネントで Shadow DOM が正当な理由なく使われている
* WARN: Shadow DOM の使用が意図的でない(理由なくデフォルトで有効化されている)

6. 品質ゲート
* FAIL: テストファイルがない
* FAIL: デモ HTML がサポートされていない props や振る舞いをデモしている
* FAIL: デモページにデザインシステムの必須 CSS/トークンが読み込まれていない
* WARN: テストが HTML スナップショット全体のアサートのみ

「完成」と言う前の最終手動チェック

自動チェックだけだと、全部は拾いきれない部分もあります。

  • フォーカス状態(ブラウザデフォルトが最も安全): 
    ページをTabで移動して、すべてのインタラクティブ要素に可視フォーカス状態があることを確認
  • リセットCSSの影響: 
    リセットは不整合なブラウザデフォルトの正規化に役立ちますが、アウトライン、ネイティブな外観、デフォルトのフォーム動作を剥がしていないか確認
  • トークンフォールバックをオフにする: 
    フォールバックはトークン読み込み失敗を隠し、「第二の」デザインシステムを作ってしまう
  • テスト実行(npm run test): 
    各コンポーネントは最低でも基本レンダリング+1つのprop駆動動作をテストすべき
  • コンソールを確認: 
    警告やエラーが出ていないことを確認する。
  • disabled+インタラクション状態の検証: 
    disabled、hover、focus、error状態が正しく動作すること — 見た目だけでなく振る舞いも確認

デザインシステムを配布して使う

コンポーネントのチェックがひと通り済んだら、最後は「使う側でそのまま読み込める形」にして渡す段階です。

つまり、デザインシステム側でビルドし、その出力を利用側プロジェクトで読み込めるようにします。

流れは2段階です。

  1. まず、システム側(your-design-system)でビルドを実行して、配布用のCSSとJavaScriptを生成します。
  2. 次に、そのビルド済みファイル一式を利用側のサイト(your-site)に配置し、数行の読み込みコードを書けばコンポーネントを使い始められます。

ステップ1 — デザインシステム側でビルドする

デザインシステム側で以下を実行します。

npm ci
npm run build

これで本番用の静的ファイルを含む dist/ フォルダが生成されます。

ビルドするのはデザインシステム側の担当者だけです。 利用者はビルドコマンドを実行する必要はありません。

ステップ2 — ビルド済みファイルをコピー

生成された dist/ フォルダを、コンポーネントを使うプロジェクトにコピーします。

your-site/
├─ dist/
│  └─ your-design-system/
│     ├─ your-design-system.css
│     ├─ your-design-system.esm.js
│     └─ assets/
└─ index.html

dist/ の中身はすべて静的なCSS、JavaScript、関連アセットです。

<link
  rel="stylesheet"
  href="./dist/your-design-system/your-design-system.css"
/>

このCSSに、コンポーネントのスタイルとデザイントークンが含まれます。

ステップ4 — JavaScriptモジュールを読み込む

<script
  type="module"
  src="./dist/your-design-system/your-design-system.esm.js"
></script>

これでブラウザにすべてのカスタム要素が登録されます。

ステップ5 — コンポーネントを使う

新しいサイトでこう使えます:

<my-button variant="primary">Label</my-button>

<my-accordion heading="Title"> Content </my-accordion>

利用側プロジェクトにStencilをインストールする必要はありません。ブラウザで読み込める形にしておけば、そのまま使えます。

ライフサイクル全体をまとめると次の流れになります:

  • 一度ビルド
  • dist/ をコピー
  • 2ファイルを読み込み
  • コンポーネントタグを書く

コンポーネント作りって楽しいのよ(!important)

デザインシステムを自分で作ると、どこでも気づくようになります。 スマホのドラッグストアアプリにも、確定申告サイトにも。(笑)

ただの「UI」じゃなく、コンポーネントが見えるようになります。

「あ、これステッパーだ。」
 「同じボタンバリアント使い回してるな。」

たとえば、メルカリで注文を確認したとき、配送完了を示す完全に埋まったステッパーを見て、「ステッパーコンポーネントにこんなに喜びを感じる日が来るなんて」と本気で思いました。

画面の裏にある見えないシステムに感謝し始める。 その視点の変化こそが、デザインシステムを作るおもしろさのひとつだと思います。

さいごに

AIを使うことで、コンポーネントのたたき台まではかなり速く作れます。
けれど、本当に差が出るのはその先の設計判断です。

ネイティブ要素をどう活かすか、Shadow DOM をどう使い分けるか、slot をどう設計するか。
そうした積み重ねが、壊れにくく再利用しやすいデザインシステムにつながっていきます。

そして、自分でデザインシステムを作ってみると、普段見ているUIがただの画面ではなく、設計された部品の集合として見えるようになります。
この視点の変化もまた、実装してみて得られる大きな学びのひとつでした。

AI × Stencil.jsで進めるデザインシステム実装ガイド Tokens Studio から Web Components へつなぐ
サムネイル