メインコンテンツへスキップ
Code & Craft
Web制作 約5分で読めます

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つの技術仕様から構成されます。

  1. Custom Elements: 独自のHTMLタグを定義する
  2. Shadow DOM: スタイルとDOMをカプセル化する
  3. 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 を併用することで、開発効率も大きく改善します。

参考リンク

#Web Components #HTML #JavaScript #フロントエンド
シェア: