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は、ブラウザ標準の仕組みだけを使い、再利用可能なUI部品(コンポーネント)を作成・配布できる技術の総称です。ReactやVue、Angularなどのフレームワークとは異なり、特定のフレームワークに依存しません。原則として、最新のモダンブラウザであれば追加ライブラリなしで利用できます(※一部Polyfillが必要な機能もあるため、古いブラウザ対応時には注意が必要です)。
こうした理由から、Web ComponentsはUIライブラリとしても、フレームワーク連携要素としても注目を集めています。
Web Componentsは大きく分けて下記3つの要素(+α)によって成り立っています。
「カスタム要素」と呼ばれる、HTMLタグの拡張機能です。自分で好きな名前のタグ(例:<my-button>
)を定義して、そのタグに対して特定の機能やUIを紐づけることができます。
従来のDOMとは切り離された“影のDOM”を作成し、スタイルやDOM構造を外部から隔離できます。これによって、コンポーネント内部の実装と外部のページスタイルが干渉しにくくなります。
あらかじめ用意したHTMLの断片やスニペットをテンプレートとして定義し、JS側で複製してDOMに挿入する技術です。特にカスタム要素の内部構造をテンプレートで定義して利用することが多いです。
かつてはHTML Importsという仕組みが標準化の一環として提案されていましたが、現在はChrome以外のブラウザで採用されなかった経緯があり、事実上非推奨とされています。代わりにES Modulesのimport機能を使うのが一般的です。
多くの企業やプロジェクトでは、フロントエンドのフレームワークが混在したり、あるいはゆくゆく別の技術スタックに移行する可能性があります。Web Componentsはブラウザが標準でサポートする仕様のため、フレームワークの種類を問わず使い回せるのが大きな魅力です。
UIコンポーネントを作り込むと、共通部品の更新が頻繁に発生します。Web Componentsにしておけば、コンポーネント単位でバージョン管理でき、アップデートも独立して行いやすくなります。
Shadow DOMを使うことで、コンポーネント内部のスタイルやスクリプトが外部と衝突しにくくなります。特に大規模サイトや複数チームが同一プロジェクトを開発する際には、想定外のレイアウト崩れやCSSの競合を回避しやすいメリットがあります。
ここでは、Web Componentsを使ったカスタム要素の最も基本的な書き方を見ていきます。
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に追加された際にテキストを表示するだけの最小限の実装になっています。
ポイント
my-element
やx-button
のような形式が一般的です。connectedCallback()
、disconnectedCallback()
などのライフサイクルコールバックを利用して、要素が追加・削除されたタイミングで処理を行えます。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>
<template>
タグ内のHTMLは、スクリプトで複製(cloneNode(true)
)しない限りDOMに描画されません。<slot>
を使うと、ユーザーがコンポーネントの外側で指定した要素をコンポーネントの内部に差し込むことができます。Custom Elementsには以下のようなライフサイクルコールバックが存在します。
constructor()
connectedCallback()
disconnectedCallback()
attributeChangedCallback(name, oldValue, newValue)
adoptedCallback()
これらを組み合わせることで、コンポーネントのライフサイクルを細かく制御できます。
Web Componentsでは、HTML要素上の属性 と JSオブジェクトのプロパティ の2つをそれぞれ制御することがよくあります。
<my-element attr="value">
のようなもの。文字列ベースで管理される。element.someProperty
など。カスタム要素で属性を扱う場合、static get observedAttributes()
を定義しておくことで、特定の属性が変化したときにattributeChangedCallback
が呼び出されるようになります。
Shadow DOM内で<slot>
タグを使うと、ユーザーがコンポーネント外側で指定したDOM要素をコンポーネント内部に挿入可能です。
たとえば、複数のスロットを使ってヘッダー、メイン、フッターを差し込めるようにすると、高い汎用性を持つコンポーネントが作れます。
HTML
<slot name="header"></slot>
<slot name="body"></slot>
<slot name="footer"></slot>
ReactやVueなどのフレームワークは独自のDOM管理やバーチャルDOMを用いますが、Web Componentsとも併用が可能です。
フレームワークのコンポーネントツリーの中で、Web Componentsを使用することで「特定の部分だけをカスタム要素として提供する」ことができます。
逆に、Web Componentsの中にReactやVueの仕組みを直接持ち込むことも技術的には可能ですが、ライフサイクルの違いなどを理解して上手に使い分ける必要があります。
より高度なWeb Components開発を行う場合、Lit や Stencil などのライブラリが人気です。
これらはバニラなWeb Componentsよりも、テンプレートバインディングやステート管理を容易にし、なおかつShadow DOMの操作や属性管理をシンプルにしてくれます。
バニラのWeb Componentsで基礎をしっかり押さえた上で、これらのライブラリを使うと開発生産性がさらに向上します。
カスタム要素の名前は必ずハイフンを含む必要がありますが、プロダクトが複数ある場合や、デザインシステム内で使う場合には、プレフィックスをつけて整理するのが望ましいです。
例 : <company-button>
, <company-card>
のように企業名やライブラリ名を付けると命名重複を防止できます。
Shadow DOMは便利ですが、影響範囲が狭い分、スタイルを大量に書くと重複が増えてしまい、パフォーマンスに影響が出ることもあります。
また、複雑なアニメーションや大きな画像を含む場合にも注意が必要です。開発時点でCSSのカスケードとコンポーネントごとの責務をよく整理しましょう。
Web Componentsも通常のHTML要素同様、アクセシビリティを確保する必要があります。
role
やaria-*
属性を適切に扱う。特にShadow DOMを使う場合、スクリーンリーダーの動作検証を入念に行うことをおすすめします。
ここでは、簡単なサンプルとして、実際に使える可能性がある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>
<custom-button>
の内側に書いたテキストが<slot>
によってボタン内に挿入される。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>
<custom-dialog>
要素を取得し、そのshow()
やhide()
メソッドを呼び出すことでダイアログを表示・非表示する。slot
を利用してダイアログヘッダーや本文、フッター(ボタンなど)を差し替えることで、汎用的に利用できる。プロダクション環境向けの開発では、BabelやTypeScriptと組み合わせたり、Polyfillを含めたりするために、RollupやWebpackといったモジュールバンドラを使用することが多いです。
ES Modulesが普及している昨今では、あえてバンドラを使わずに<script type="module">
だけで開発するケースも増えていますが、大規模開発やレガシーブラウザ対応を考慮すると、適切なビルド環境の導入は依然として有効です。
開発中はローカルサーバーを立ててブラウザで動作確認をするのが一般的です。
npm
やyarn
などのパッケージマネージャを使うなら、npm run serve
やnpm start
のようなスクリプトを用意して簡易サーバーを立ち上げると便利です。
Web Componentsはブラウザ標準の技術だけでフレームワークから独立した再利用可能なUIコンポーネントを開発できる強力な手段です。Shadow DOMによるカプセル化やCustom Elementsによる拡張性など、学ぶべきポイントは多岐にわたりますが、その分メリットも大きいです。長期的かつ大規模なプロジェクトで真価を発揮する可能性が高く、加えてReact/Vue/Angularなどのフレームワークとの共存も可能です。
まずはシンプルなカスタム要素を作成し、Shadow DOMの基本を押さえ、必要に応じてテンプレートやスロットなどを活用しましょう。そこから発展的なライブラリ(LitやStencilなど)を導入することで、より効率的・高機能なコンポーネント開発へと進むことができます。