Web Components 2025年最新ガイド
Web Componentsは、フレームワークに依存しない再利用可能なコンポーネントを作成するためのWeb標準技術群です。2025年現在、主要ブラウザでの対応が完了し、実践的な開発が可能になっています。
Web Componentsとは
Web Componentsは以下の3つの主要技術で構成されています。
1. Custom Elements
独自のHTMLタグを定義する仕組みです。
class MyButton extends HTMLElement {
constructor() {
super();
this.addEventListener('click', () => {
console.log('Button clicked!');
});
}
connectedCallback() {
this.innerHTML = `
<button class="my-button">
${this.getAttribute('label') || 'Click me'}
</button>
`;
}
}
customElements.define('my-button', MyButton);
使用例:
<my-button label="送信"></my-button>
2. Shadow DOM
カプセル化されたDOMツリーを作成し、スタイルの衝突を防ぎます。
class CardComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.title {
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 8px;
}
.content {
color: #666;
}
</style>
<div class="card">
<div class="title"><slot name="title">タイトル</slot></div>
<div class="content"><slot></slot></div>
</div>
`;
}
}
customElements.define('card-component', CardComponent);
使用例:
<card-component>
<span slot="title">お知らせ</span>
<p>これはカードコンポーネントのコンテンツです。</p>
</card-component>
3. HTML Templates
再利用可能なマークアップの雛形を定義します。
<template id="product-card">
<style>
.product {
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 12px;
margin: 8px;
}
.product-name {
font-weight: bold;
color: #333;
}
.product-price {
color: #e53935;
font-size: 1.2rem;
}
</style>
<div class="product">
<div class="product-name"></div>
<div class="product-price"></div>
</div>
</template>
<script>
class ProductCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
const template = document.getElementById('product-card');
const content = template.content.cloneNode(true);
content.querySelector('.product-name').textContent = this.getAttribute('name');
content.querySelector('.product-price').textContent = `¥${this.getAttribute('price')}`;
this.shadowRoot.appendChild(content);
}
}
customElements.define('product-card', ProductCard);
</script>
ライフサイクルコールバック
Custom Elementsには4つのライフサイクルメソッドがあります。
class LifecycleDemo extends HTMLElement {
// 要素が作成されたとき
constructor() {
super();
console.log('constructor');
}
// DOMに追加されたとき
connectedCallback() {
console.log('connectedCallback');
this.render();
}
// DOMから削除されたとき
disconnectedCallback() {
console.log('disconnectedCallback');
// クリーンアップ処理
}
// 属性が変更されたとき
attributeChangedCallback(name, oldValue, newValue) {
console.log(`attributeChangedCallback: ${name} = ${newValue}`);
this.render();
}
// 監視する属性を指定
static get observedAttributes() {
return ['title', 'count'];
}
render() {
this.innerHTML = `
<h2>${this.getAttribute('title') || 'Default Title'}</h2>
<p>Count: ${this.getAttribute('count') || 0}</p>
`;
}
}
customElements.define('lifecycle-demo', LifecycleDemo);
実践例: カウンターコンポーネント
状態管理を含む完全なコンポーネントの例です。
class CounterComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._count = 0;
}
connectedCallback() {
this.render();
this.shadowRoot.querySelector('.increment').addEventListener('click', () => {
this.count++;
});
this.shadowRoot.querySelector('.decrement').addEventListener('click', () => {
this.count--;
});
this.shadowRoot.querySelector('.reset').addEventListener('click', () => {
this.count = 0;
});
}
get count() {
return this._count;
}
set count(value) {
this._count = value;
this.render();
// カスタムイベントを発火
this.dispatchEvent(new CustomEvent('countchange', {
detail: { count: this._count },
bubbles: true,
composed: true
}));
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: inline-block;
font-family: system-ui;
}
.counter {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border: 2px solid #2196F3;
border-radius: 8px;
background: #f5f5f5;
}
.count {
font-size: 2rem;
font-weight: bold;
min-width: 60px;
text-align: center;
}
button {
padding: 8px 16px;
font-size: 1rem;
border: none;
border-radius: 4px;
background: #2196F3;
color: white;
cursor: pointer;
transition: background 0.2s;
}
button:hover {
background: #1976D2;
}
.reset {
background: #f44336;
}
.reset:hover {
background: #d32f2f;
}
</style>
<div class="counter">
<button class="decrement">-</button>
<div class="count">${this._count}</div>
<button class="increment">+</button>
<button class="reset">Reset</button>
</div>
`;
}
}
customElements.define('counter-component', CounterComponent);
使用例:
<counter-component></counter-component>
<script>
document.querySelector('counter-component').addEventListener('countchange', (e) => {
console.log('Count changed:', e.detail.count);
});
</script>
スロットの活用
スロットを使うと、外部からコンテンツを注入できます。
class TabsComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
.tabs {
display: flex;
border-bottom: 2px solid #e0e0e0;
gap: 8px;
}
.tab-content {
padding: 16px;
border: 1px solid #e0e0e0;
border-top: none;
}
</style>
<div class="tabs">
<slot name="tabs"></slot>
</div>
<div class="tab-content">
<slot name="content"></slot>
</div>
`;
}
}
customElements.define('tabs-component', TabsComponent);
使用例:
<tabs-component>
<button slot="tabs">Tab 1</button>
<button slot="tabs">Tab 2</button>
<button slot="tabs">Tab 3</button>
<div slot="content">
<p>This is the content area</p>
</div>
</tabs-component>
フォーム統合
Web Componentsをフォームと統合する方法です。
class CustomInput extends HTMLElement {
static formAssociated = true;
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._internals = this.attachInternals();
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
input {
padding: 8px 12px;
font-size: 1rem;
border: 2px solid #ccc;
border-radius: 4px;
outline: none;
transition: border-color 0.2s;
}
input:focus {
border-color: #2196F3;
}
:host([invalid]) input {
border-color: #f44336;
}
</style>
<input type="text" />
`;
const input = this.shadowRoot.querySelector('input');
input.addEventListener('input', (e) => {
this._internals.setFormValue(e.target.value);
// バリデーション
if (e.target.value.length < 3) {
this._internals.setValidity(
{ tooShort: true },
'3文字以上入力してください'
);
this.setAttribute('invalid', '');
} else {
this._internals.setValidity({});
this.removeAttribute('invalid');
}
});
}
get value() {
return this._internals.value;
}
set value(val) {
this._internals.setFormValue(val);
this.shadowRoot.querySelector('input').value = val;
}
}
customElements.define('custom-input', CustomInput);
リアクティブプロパティ
属性とプロパティを同期する実装パターンです。
class ReactiveComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._data = {
title: '',
count: 0
};
}
static get observedAttributes() {
return ['title', 'count'];
}
attributeChangedCallback(name, oldValue, newValue) {
this[name] = newValue;
}
get title() {
return this._data.title;
}
set title(value) {
this._data.title = value;
this.setAttribute('title', value);
this.render();
}
get count() {
return this._data.count;
}
set count(value) {
this._data.count = parseInt(value, 10);
this.setAttribute('count', value);
this.render();
}
connectedCallback() {
this.render();
}
render() {
this.shadowRoot.innerHTML = `
<style>
.container {
padding: 16px;
border: 1px solid #ddd;
border-radius: 4px;
}
</style>
<div class="container">
<h3>${this._data.title}</h3>
<p>Count: ${this._data.count}</p>
</div>
`;
}
}
customElements.define('reactive-component', ReactiveComponent);
パフォーマンス最適化
1. レンダリングの最適化
class OptimizedComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._renderScheduled = false;
}
scheduleRender() {
if (this._renderScheduled) return;
this._renderScheduled = true;
requestAnimationFrame(() => {
this.render();
this._renderScheduled = false;
});
}
attributeChangedCallback() {
this.scheduleRender();
}
render() {
// レンダリング処理
}
}
2. メモリリーク防止
class SafeComponent extends HTMLElement {
connectedCallback() {
this._handleClick = () => console.log('clicked');
this.addEventListener('click', this._handleClick);
}
disconnectedCallback() {
// イベントリスナーを削除
this.removeEventListener('click', this._handleClick);
}
}
TypeScript対応
型安全なWeb Componentsの実装例です。
interface CounterState {
count: number;
step: number;
}
class TypedCounter extends HTMLElement {
private _state: CounterState;
private _shadowRoot: ShadowRoot;
constructor() {
super();
this._shadowRoot = this.attachShadow({ mode: 'open' });
this._state = {
count: 0,
step: 1
};
}
connectedCallback(): void {
this.render();
this.attachEventListeners();
}
private attachEventListeners(): void {
const incrementBtn = this._shadowRoot.querySelector('.increment') as HTMLButtonElement;
const decrementBtn = this._shadowRoot.querySelector('.decrement') as HTMLButtonElement;
incrementBtn?.addEventListener('click', () => this.increment());
decrementBtn?.addEventListener('click', () => this.decrement());
}
private increment(): void {
this._state.count += this._state.step;
this.render();
}
private decrement(): void {
this._state.count -= this._state.step;
this.render();
}
private render(): void {
this._shadowRoot.innerHTML = `
<style>
.counter { padding: 16px; }
button { margin: 0 8px; }
</style>
<div class="counter">
<button class="decrement">-</button>
<span>${this._state.count}</span>
<button class="increment">+</button>
</div>
`;
}
}
customElements.define('typed-counter', TypedCounter);
ベストプラクティス
1. 命名規則
// 必ずハイフンを含める
customElements.define('my-component', MyComponent); // ✓ 正しい
customElements.define('mycomponent', MyComponent); // ✗ エラー
2. アクセシビリティ
class AccessibleButton extends HTMLElement {
connectedCallback() {
// ARIA属性を設定
this.setAttribute('role', 'button');
this.setAttribute('tabindex', '0');
this.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.click();
}
});
}
}
3. プログレッシブエンハンスメント
if ('customElements' in window) {
import('./my-component.js');
} else {
// フォールバック処理
console.warn('Custom Elements not supported');
}
既存フレームワークとの連携
React
import { useRef, useEffect } from 'react';
function App() {
const counterRef = useRef(null);
useEffect(() => {
const handler = (e) => {
console.log('Count:', e.detail.count);
};
counterRef.current?.addEventListener('countchange', handler);
return () => {
counterRef.current?.removeEventListener('countchange', handler);
};
}, []);
return <counter-component ref={counterRef} />;
}
Vue
<template>
<counter-component @countchange="handleCountChange" />
</template>
<script setup>
const handleCountChange = (e) => {
console.log('Count:', e.detail.count);
};
</script>
まとめ
Web Componentsは、フレームワーク非依存の再利用可能なコンポーネントを作成するための強力な標準技術です。2025年現在、主要ブラウザでの対応が完了し、実践的な開発が可能になっています。
特にデザインシステムやUIライブラリの構築、マイクロフロントエンドアーキテクチャでの活用が期待されています。Shadow DOMによるスタイルのカプセル化、Custom Elementsによる独自タグの定義、HTML Templatesによる再利用可能なマークアップなど、Web標準の力を最大限活用できる技術です。