Web Components 入門ガイド|カスタム要素でフレームワーク独立のUIを作る【2026年版】
Web Components(Custom Elements・Shadow DOM・HTML Templates)の使い方を実例で解説。React/Vue に依存しない再利用可能なUIコンポーネントの作り方を完全網羅。
React、Vue、Svelte … フレームワークの選択肢が増える中、「どのフレームワークでも動く再利用可能なコンポーネント」を作る標準技術が Web Components です。2026年現在、全主要ブラウザでサポートされ、デザインシステムや共通UIの実装で注目されています。
Web Components の3つの柱
Web Components は3つの技術仕様から構成されます。
- Custom Elements: 独自のHTMLタグを定義する
- Shadow DOM: スタイルとDOMをカプセル化する
- HTML Templates:
<template>と<slot>で再利用可能なマークアップを作る
Custom Elements の基本
最小構成の Custom Element:
class MyGreeting extends HTMLElement {
connectedCallback() {
this.innerHTML = `<p>こんにちは、${this.getAttribute('name')}さん!</p>`;
}
}
customElements.define('my-greeting', MyGreeting);
<my-greeting name="太郎"></my-greeting>
<!-- 結果: <p>こんにちは、太郎さん!</p> -->
命名ルール: Custom Element の名前は必ずハイフンを含む必要があります(my-greeting は OK、mygreeting は NG)。これにより、標準HTMLタグとの衝突を防ぎます。
ライフサイクルコールバック
Custom Element には以下のライフサイクルがあります。
class MyElement extends HTMLElement {
constructor() {
super();
console.log('作成された');
}
connectedCallback() {
console.log('DOM に追加された');
}
disconnectedCallback() {
console.log('DOM から削除された');
}
adoptedCallback() {
console.log('別のドキュメントに移動した');
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(`${name} が ${oldValue} から ${newValue} に変更された`);
}
static get observedAttributes() {
return ['name', 'color']; // 監視する属性
}
}
React の useEffect や Vue の mounted に相当する機能が、標準 API として提供されています。
Shadow DOM
Shadow DOM は、要素の内部にカプセル化された DOM ツリーを作ります。外部 CSS の影響を受けず、内部のスタイルも外に漏れません。
class StyledButton extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
button {
background: #0284c7;
color: white;
padding: 0.5rem 1rem;
border: none;
border-radius: 0.5rem;
cursor: pointer;
font-family: inherit;
}
button:hover {
background: #0369a1;
}
</style>
<button><slot></slot></button>
`;
}
}
customElements.define('styled-button', StyledButton);
<styled-button>クリック</styled-button>
このボタンは、どのページに配置しても同じスタイルで表示されます。ページ側の CSS は内部に影響しません。
要素で中身を差し込む
React の children と同じ概念です。
shadow.innerHTML = `
<div class="card">
<div class="card-header"><slot name="header"></slot></div>
<div class="card-body"><slot></slot></div>
<div class="card-footer"><slot name="footer"></slot></div>
</div>
`;
<my-card>
<h2 slot="header">タイトル</h2>
<p>本文コンテンツ</p>
<button slot="footer">アクション</button>
</my-card>
名前付き <slot> で複数の挿入ポイントを作れます。
実践例: トースト通知コンポーネント
class ToastNotification extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
const type = this.getAttribute('type') || 'info';
const duration = parseInt(this.getAttribute('duration') || '3000');
this.shadowRoot.innerHTML = `
<style>
:host {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 1000;
}
.toast {
padding: 1rem 1.5rem;
border-radius: 0.5rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
animation: slideIn 0.3s ease-out;
font-family: sans-serif;
}
.toast.info { background: #0284c7; color: white; }
.toast.success { background: #22c55e; color: white; }
.toast.error { background: #ef4444; color: white; }
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
</style>
<div class="toast ${type}">
<slot></slot>
</div>
`;
setTimeout(() => {
this.remove();
}, duration);
}
}
customElements.define('toast-notification', ToastNotification);
<button onclick="showToast()">保存</button>
<script>
function showToast() {
const toast = document.createElement('toast-notification');
toast.setAttribute('type', 'success');
toast.textContent = '保存しました!';
document.body.appendChild(toast);
}
</script>
フレームワーク不要で、どのプロジェクトでも使い回せます。
Lit でより効率的に
素の Web Components は冗長になりがちです。Lit を使うと、React のような宣言的な記法で書けます。
import { LitElement, html, css } from 'lit';
class MyCounter extends LitElement {
static properties = {
count: { type: Number },
};
static styles = css`
button { padding: 0.5rem 1rem; }
p { font-size: 1.5rem; }
`;
constructor() {
super();
this.count = 0;
}
render() {
return html`
<p>カウント: ${this.count}</p>
<button @click=${() => this.count++}>+1</button>
`;
}
}
customElements.define('my-counter', MyCounter);
Lit のライブラリサイズは 5KB 程度と軽量で、ランタイム依存もありません。
React / Vue / Svelte との共存
Web Components は、あらゆるフレームワーク内で使用できます。
React
import 'styled-button.js'; // カスタム要素を登録
function App() {
return <styled-button>クリック</styled-button>;
}
Vue
<template>
<styled-button>クリック</styled-button>
</template>
Vue では app.config.compilerOptions.isCustomElement = tag => tag.includes('-') を設定する必要があります。
Svelte
Svelte 5 以降では、Custom Elements をネイティブにサポートしています。
Web Components を使うべきケース
向いている用途
- デザインシステム: 社内で横断的に使うUIコンポーネント
- 埋め込みウィジェット: 他社サイトに貼り付けるチャットボタン等
- マイクロフロントエンド: 複数フレームワークが混在するプロジェクト
- ブラウザ拡張: コンテキスト独立のUI
向いていない用途
- SPA全体: React/Vue の方が開発効率が良い
- 状態管理が複雑: フレームワークの力を借りた方が楽
まとめ
Web Components は「フレームワーク独立」「カプセル化」「標準技術」という強みを持つ UI コンポーネント実装手段です。
主要な利点:
- フレームワークを問わず動作する
- Shadow DOM によるスタイルのカプセル化
- 標準技術なので将来性が高い
- 軽量(ライブラリ不要)
まずはトーストやモーダルなど、単機能のコンポーネントから始めるのがおすすめです。Lit を併用することで、開発効率も大きく改善します。