Web Programming

Web Components are a browser-native way to define and encapsulate custom HTML elements, allowing you to create reusable UI components without relying on any specific framework.

Web Components

Web Componentsとは

Web Componentsは、ブラウザ標準の仕組みだけを使い、再利用可能なUI部品(コンポーネント)を作成・配布できる技術の総称です。ReactやVue、Angularなどのフレームワークとは異なり、特定のフレームワークに依存しません。原則として、最新のモダンブラウザであれば追加ライブラリなしで利用できます(※一部Polyfillが必要な機能もあるため、古いブラウザ対応時には注意が必要です)。

Web Componentsが解決しようとしている問題

再利用性
HTML/CSS/JSの組み合わせを別のプロジェクトや環境に簡単に持ち込める。
複雑化の抑制
大規模プロジェクトでよく発生する、CSSやJSが衝突する問題を「スコープ分離」によって解消。
フレームワーク縛りからの解放
長期メンテナンスやマイクロフロントエンド構成などで、「フレームワークを乗り換える自由度」を得られる。

こうした理由から、Web ComponentsはUIライブラリとしても、フレームワーク連携要素としても注目を集めています。

Web Componentsを構成する主要技術

Web Componentsは大きく分けて下記3つの要素(+α)によって成り立っています。

Custom Elements

「カスタム要素」と呼ばれる、HTMLタグの拡張機能です。自分で好きな名前のタグ(例:<my-button>)を定義して、そのタグに対して特定の機能やUIを紐づけることができます。

Shadow DOM

従来のDOMとは切り離された“影のDOM”を作成し、スタイルやDOM構造を外部から隔離できます。これによって、コンポーネント内部の実装と外部のページスタイルが干渉しにくくなります。

Templates (HTML Templates)

あらかじめ用意したHTMLの断片やスニペットをテンプレートとして定義し、JS側で複製してDOMに挿入する技術です。特にカスタム要素の内部構造をテンプレートで定義して利用することが多いです。

(参考) HTML Importsの現状

かつてはHTML Importsという仕組みが標準化の一環として提案されていましたが、現在はChrome以外のブラウザで採用されなかった経緯があり、事実上非推奨とされています。代わりにES Modulesのimport機能を使うのが一般的です。

Web Componentsが注目される理由

フレームワークに依存しない再利用可能なコンポーネント

多くの企業やプロジェクトでは、フロントエンドのフレームワークが混在したり、あるいはゆくゆく別の技術スタックに移行する可能性があります。Web Componentsはブラウザが標準でサポートする仕様のため、フレームワークの種類を問わず使い回せるのが大きな魅力です。

メンテナンス性の向上

UIコンポーネントを作り込むと、共通部品の更新が頻繁に発生します。Web Componentsにしておけば、コンポーネント単位でバージョン管理でき、アップデートも独立して行いやすくなります。

スコープ分離によるスタイル・機能の衝突防止

Shadow DOMを使うことで、コンポーネント内部のスタイルやスクリプトが外部と衝突しにくくなります。特に大規模サイトや複数チームが同一プロジェクトを開発する際には、想定外のレイアウト崩れやCSSの競合を回避しやすいメリットがあります。

Web Componentsの基本構文と実装例

ここでは、Web Componentsを使ったカスタム要素の最も基本的な書き方を見ていきます。

Custom Elements定義の基本例

HTML

<!-- カスタム要素の使用例 -->
<my-greeting></my-greeting>

<script>
	// カスタム要素用のクラスを定義
	class MyGreeting extends HTMLElement {
		constructor() {
			super();
			// コンストラクタ内で初期化処理を行う(DOM操作はまだ非推奨)
			console.log('MyGreeting constructed!');
		}

		// ライフサイクルコールバック: 要素がDOMに追加された時
		connectedCallback() {
			this.textContent = 'Hello, Web Components!';
		}
	}

	// カスタム要素として登録する
	customElements.define('my-greeting', MyGreeting);
</script>

上記の例では、my-greeting というカスタム要素を定義し、DOMに追加された際にテキストを表示するだけの最小限の実装になっています。

ポイント

Shadow DOMを使ったカプセル化

Shadow DOMを使うと、コンポーネント内部だけに閉じたDOMを生成できます。

HTML

<my-shadow-element></my-shadow-element>

<script>
	class MyShadowElement extends HTMLElement {
		constructor() {
			super();
			// Shadow Rootを"open"モードで作成
			const shadow = this.attachShadow({ mode: 'open' });
			
			// コンテンツを挿入
			shadow.innerHTML = `
				<style>
					/* このスタイルはshadow root内のみで適用される */
					.text {
						font-size: 20px;
						color: #007acc; 
					}
				</style>
				<div class="text">This is shadow DOM content!</div>
			`;
		}
	}

	customElements.define('my-shadow-element', MyShadowElement);
</script>

上記の例では、Shadow DOM内の.textクラスに適用したスタイルは外部のCSSと干渉しません。逆に、外部のCSSルールが.textに対して作用することもありません。これが「カプセル化」によるメリットです。

テンプレート活用例

HTMLテンプレートを使う場合は、<template>タグで定義し、それをJSから複製してShadow DOMやLight DOMに挿入します。

HTML

<!-- テンプレート定義 -->
<template id="my-card-template">
	<style>
		.card {
			border: 1px solid #ccc;
			padding: 16px;
		}
	</style>
	<div class="card">
		<slot name="content"></slot>
	</div>
</template>

<my-card>
	<!-- スロットに差し込みたいコンテンツに slot 属性を付与 -->
	<span slot="content">カードの内容です。</span>
</my-card>

<script>
	class MyCard extends HTMLElement {
		constructor() {
			super();
			const shadow = this.attachShadow({ mode: 'open' });
			
			// テンプレート要素を取得し複製
			const template = document.getElementById('my-card-template');
			const content = template.content.cloneNode(true);

			shadow.appendChild(content);
		}
	}

	customElements.define('my-card', MyCard);
</script>
カードの内容です。

より発展的なトピック

ライフサイクルコールバックの活用

Custom Elementsには以下のようなライフサイクルコールバックが存在します。

constructor()
インスタンス生成時に呼び出される(DOMにはまだ追加されていない)。
connectedCallback()
DOMツリーに要素が追加されたときに呼び出される。
disconnectedCallback()
DOMツリーから要素が削除されたときに呼び出される。
attributeChangedCallback(name, oldValue, newValue)
要素に設定された属性が変更された時に呼び出される。
adoptedCallback()
要素が別のDocumentに移動された時に呼び出される(あまり利用ケースは多くない)。

これらを組み合わせることで、コンポーネントのライフサイクルを細かく制御できます。

属性 (attributes) とプロパティ (properties) の違い

Web Componentsでは、HTML要素上の属性 と JSオブジェクトのプロパティ の2つをそれぞれ制御することがよくあります。

属性 (attributes)
HTMLタグに書かれた<my-element attr="value">のようなもの。文字列ベースで管理される。
プロパティ (properties)
要素のインスタンス(JSオブジェクト)としてアクセスするelement.somePropertyなど。

カスタム要素で属性を扱う場合、static get observedAttributes()を定義しておくことで、特定の属性が変化したときにattributeChangedCallbackが呼び出されるようになります。

スロット化 (Slots) の基礎

Shadow DOM内で<slot>タグを使うと、ユーザーがコンポーネント外側で指定したDOM要素をコンポーネント内部に挿入可能です。

たとえば、複数のスロットを使ってヘッダー、メイン、フッターを差し込めるようにすると、高い汎用性を持つコンポーネントが作れます。

HTML

<slot name="header"></slot>
<slot name="body"></slot>
<slot name="footer"></slot>

Web Componentsと各種フレームワークの比較・統合

ReactやVueとの併用

ReactやVueなどのフレームワークは独自のDOM管理やバーチャルDOMを用いますが、Web Componentsとも併用が可能です。

フレームワークのコンポーネントツリーの中で、Web Componentsを使用することで「特定の部分だけをカスタム要素として提供する」ことができます。

逆に、Web Componentsの中にReactやVueの仕組みを直接持ち込むことも技術的には可能ですが、ライフサイクルの違いなどを理解して上手に使い分ける必要があります。

ライブラリ (Lit, Stencilなど) の活用

より高度なWeb Components開発を行う場合、Lit や Stencil などのライブラリが人気です。

これらはバニラなWeb Componentsよりも、テンプレートバインディングやステート管理を容易にし、なおかつShadow DOMの操作や属性管理をシンプルにしてくれます。

Lit
Googleが中心となって開発している軽量なライブラリ。テンプレートリテラルを用いてシンプルにコンポーネントを定義可能。
Stencil
Ionicフレームワークを開発しているチームが提供しているコンパイラ型のライブラリ。TypeScript対応が充実している。

バニラのWeb Componentsで基礎をしっかり押さえた上で、これらのライブラリを使うと開発生産性がさらに向上します。

ベストプラクティスと注意点

ネーミング規則と命名衝突防止

カスタム要素の名前は必ずハイフンを含む必要がありますが、プロダクトが複数ある場合や、デザインシステム内で使う場合には、プレフィックスをつけて整理するのが望ましいです。

: <company-button>, <company-card> のように企業名やライブラリ名を付けると命名重複を防止できます。

スタイル設計とパフォーマンス最適化

Shadow DOMは便利ですが、影響範囲が狭い分、スタイルを大量に書くと重複が増えてしまい、パフォーマンスに影響が出ることもあります。

また、複雑なアニメーションや大きな画像を含む場合にも注意が必要です。開発時点でCSSのカスケードとコンポーネントごとの責務をよく整理しましょう。

アクセシビリティへの配慮

Web Componentsも通常のHTML要素同様、アクセシビリティを確保する必要があります。

特にShadow DOMを使う場合、スクリーンリーダーの動作検証を入念に行うことをおすすめします。

実例紹介: Web Componentsを使ったUIコンポーネント

ここでは、簡単なサンプルとして、実際に使える可能性があるUIコンポーネントの例を紹介します。

シンプルなカスタムボタン

HTML

<template id="custom-button-template">
	<style>
		button {
			padding: 8px 16px;
			border: none;
			cursor: pointer;
		}
	</style>
	<button>
		<slot></slot>
	</button>
</template>

<custom-button>クリック</custom-button>

<script>
	class CustomButton extends HTMLElement {
		constructor() {
			super();
			const shadow = this.attachShadow({ mode: 'open' });
			const template = document.getElementById('custom-button-template');
			shadow.appendChild(template.content.cloneNode(true));
		}

		connectedCallback() {
			const button = this.shadowRoot.querySelector('button');
			button.addEventListener('click', () => {
				alert('Button clicked!');
			});
		}
	}
	customElements.define('custom-button', CustomButton);
</script>
クリック

カスタムダイアログ

HTML

<template id="custom-dialog-template">
	<style>
		.dialog-backdrop {
			position: fixed;
			top: 0; left: 0;
			width: 100vw; height: 100vh;
			background: rgba(0, 0, 0, 0.5);
			display: none; /* 初期は非表示 */
			align-items: center;
			justify-content: center;
		}
		.dialog-box {
			background: #fff;
			padding: 20px;
			border-radius: 4px;
			min-width: 300px;
			max-width: 500px;
		}
		.dialog-header, .dialog-footer {
			margin-bottom: 10px;
		}
		.dialog-footer {
			text-align: right;
		}
	</style>
	<div class="dialog-backdrop">
		<div class="dialog-box">
			<div class="dialog-header">
				<slot name="header"></slot>
			</div>
			<div class="dialog-body">
				<slot name="body"></slot>
			</div>
			<div class="dialog-footer">
				<slot name="footer"></slot>
			</div>
		</div>
	</div>
</template>

<custom-dialog id="myDialog">
	<span slot="header">ダイアログのタイトル</span>
	<p slot="body">ここに本文が入ります。</p>
	<button slot="footer" id="closeBtn">閉じる</button>
</custom-dialog>

<script>
	class CustomDialog extends HTMLElement {
		constructor() {
			super();
			this._shadow = this.attachShadow({ mode: 'open' });
			const template = document.getElementById('custom-dialog-template');
			this._shadow.appendChild(template.content.cloneNode(true));

			this._backdrop = this._shadow.querySelector('.dialog-backdrop');
		}

		connectedCallback() {
			// slotで指定したボタンが押されたら閉じる
			const closeBtn = this.querySelector('#closeBtn');
			if (closeBtn) {
				closeBtn.addEventListener('click', () => this.hide());
			}
		}

		show() {
			this._backdrop.style.display = 'flex';
		}

		hide() {
			this._backdrop.style.display = 'none';
		}
	}
	customElements.define('custom-dialog', CustomDialog);

	// ダイアログを制御するサンプル
	document.getElementById('myDialog').show();
</script>

開発・デバッグ環境とツールチェーン

カスタムビルド (Rollup, Webpackなど)

プロダクション環境向けの開発では、BabelやTypeScriptと組み合わせたり、Polyfillを含めたりするために、RollupやWebpackといったモジュールバンドラを使用することが多いです。

ES Modulesが普及している昨今では、あえてバンドラを使わずに<script type="module">だけで開発するケースも増えていますが、大規模開発やレガシーブラウザ対応を考慮すると、適切なビルド環境の導入は依然として有効です。

ローカルサーバーでのテスト

開発中はローカルサーバーを立ててブラウザで動作確認をするのが一般的です。

npmyarnなどのパッケージマネージャを使うなら、npm run servenpm startのようなスクリプトを用意して簡易サーバーを立ち上げると便利です。

ブラウザデベロッパーツールの活用

Elementsパネル
Shadow DOM内部を確認できる(Chrome DevToolsでは、shadow rootが表示される)。
Console
ライフサイクルコールバックの呼び出しタイミングをログ出力してデバッグが可能。
Networkパネル
カスタム要素で利用するリソースが正しく読み込まれているか確認。

まとめとこれからの展望

Web Componentsに関するエコシステム

UIライブラリやコンポーネント集
Web Componentsベースで作られたコンポーネントライブラリ(Vaadin, Ionic, Shoelace など)が多数存在します。
マイクロフロントエンド
複数チームが独立したアプリケーションをビルドし、それぞれをWeb Componentsとして統合する手法が注目されています。

さらなる学習リソース

MDN Web Docs
Web Componentsの公式ドキュメント。詳しいチュートリアルあり。
Web.dev
Google主催のWeb向けコンテンツでShadow DOMやカスタム要素の概要記事が充実。
Lit公式ドキュメント
Web ComponentsのラッパーライブラリとしてLitを使う場合に便利。
Stencil公式サイト
TypeScriptを活用した効率的なコンポーネント開発に興味がある場合。

結論

Web Componentsはブラウザ標準の技術だけでフレームワークから独立した再利用可能なUIコンポーネントを開発できる強力な手段です。Shadow DOMによるカプセル化やCustom Elementsによる拡張性など、学ぶべきポイントは多岐にわたりますが、その分メリットも大きいです。長期的かつ大規模なプロジェクトで真価を発揮する可能性が高く、加えてReact/Vue/Angularなどのフレームワークとの共存も可能です。

まずはシンプルなカスタム要素を作成し、Shadow DOMの基本を押さえ、必要に応じてテンプレートやスロットなどを活用しましょう。そこから発展的なライブラリ(LitやStencilなど)を導入することで、より効率的・高機能なコンポーネント開発へと進むことができます。