$host - カスタムエレメント

$host() は、Svelte 5 で導入されたルーンで、カスタムエレメント(Web Components)としてコンパイルされたコンポーネントの内側から、そのホスト要素(DOM ノード)にアクセスするために使います。React・Vue・Angular など Svelte の外にあるフレームワークや素の HTML に対して、Web 標準の形でコンポーネントを提供したいときに必要になります。

代表的な用途は、コンポーネントの状態変化を外の世界に通知する カスタムイベントの発火 です。

<!-- Counter.svelte -->
<svelte:options customElement="my-counter" />

<script lang="ts">
  let count = $state(0);

  function increment() {
    count++;
    // <my-counter> 自身からカスタムイベントを発火
    $host().dispatchEvent(
      new CustomEvent('countchange', { detail: { count } })
    );
  }
</script>

<button onclick={increment}>+{count}</button>

$host() は <svelte:options customElement="..." /> を指定したコンポーネントの中でのみ使用可能 です。通常の Svelte コンポーネント内で呼び出すとコンパイル時にエラーになります。

カスタムエレメントとは

カスタムエレメント(Web Components)は、再利用可能なカスタムHTML要素を定義するWeb標準技術です。Svelteコンポーネントをカスタムエレメントとしてコンパイルすることで、Svelte以外の環境でも使用できるようになります。

さらに詳しく学ぶ

カスタムエレメント(Web Components)についての詳細は、以下のリソースをご参照ください。

MDN Web Docs

  • 📖 [Web Components | MDN](https://developer.mozilla.org/ja/docs/Web/API/Web_components)
  • 📖 [カスタムエレメントの使用 | MDN](https://developer.mozilla.org/ja/docs/Web/API/Web_components/Using_custom_elements)
  • 📖 [Shadow DOM の使用 | MDN](https://developer.mozilla.org/ja/docs/Web/API/Web_components/Using_shadow_DOM)
  • 📖 [HTMLElement | MDN](https://developer.mozilla.org/ja/docs/Web/API/HTMLElement)

TypeScriptでのWebComponentsの学習リソース

  • 🎓 [WebComponents完全ガイド - TypeScriptで学ぶWebComponents](https://shuji-bonji.github.io/WebComponents-with-TypeScript/concepts/webcomponents-overview.html)

カスタムエレメントの定義

<!-- Counter.svelte(Web Component 化サンプル:initial を初期値スナップショットとして
     内部 state にコピーするため state_referenced_locally 警告が出るが、Web Components
     の attribute は基本的に初回のみ反映するパターンなので意図的) -->
<svelte:options customElement="my-counter" />

<script lang="ts">
  let { initial = 0 } = $props();
  let count = $state(initial);

  function increment() {
    count++;
    // カウント変更をカスタムイベントで通知
    $host().dispatchEvent(
      new CustomEvent('countchange', {
        detail: { count },
        bubbles: true
      })
    );
  }
</script>

<div class="counter">
  <button onclick={increment}>+</button>
  <span>{count}</span>
</div>

<style>
  .counter {
    display: flex;
    align-items: center;
    gap: 10px;
  }
</style>

他フレームワークでの使用例

// React
function App() {
  useEffect(() => {
    const handleCountChange = (e) => {
      console.log('Count:', e.detail.count);
    };

    const counter = document.getElementById('counter');
    counter?.addEventListener('countchange', handleCountChange);

    return () => {
      counter?.removeEventListener('countchange', handleCountChange);
    };
  }, []);

  // JSXで使用
  return React.createElement('my-counter', { id: 'counter', initial: '10' });
}
<!-- Vue.jsでの使用例 -->
<!-- Template部分 -->
<div id="app">
  <my-counter :initial="10" @countchange="handleCountChange"> </my-counter>
</div>

<!-- Script部分 -->
<script>
  new Vue({
    el: '#app',
    methods: {
      handleCountChange(event) {
        console.log('Count:', event.detail.count);
      },
    },
  });
</script>

実践例

$hostを使ったカスタムエレメントの作成から使用まで、実際のプロジェクトでの完全な手順を説明します。

ステップ1: Svelteプロジェクトの作成

まず、新しいSvelteプロジェクトを作成します。

# Svelteプロジェクトの作成
% npx sv create my-custom-elements
Need to install the following packages:
sv@0.9.2
Ok to proceed? (y) y

┌  Welcome to the Svelte CLI! (v0.9.2)
│
◇  Which template would you like?
│  SvelteKit minimal
│
◇  Add type checking with TypeScript?
│  Yes, using TypeScript syntax
│
◆  Project created
│
◇  What would you like to add to your project? (use arrow keys / space bar)
│  none
│
◇  Which package manager do you want to install dependencies with?
│  npm
│
◆  Successfully installed dependencies
│
◇  What's next? ───────────────────────────────╮
│                                              │
│  📁 Project steps                            │
│                                              │
│    1: cd my-custom-elements                  │
│    2: npm run dev -- --open                  │
│                                              │
│  To close the dev server, hit Ctrl-C         │
│                                              │
│  Stuck? Visit us at https://svelte.dev/chat  │
│                                              │
├──────────────────────────────────────────────╯
│
└  You're all set!

cd my-custom-elements

# カスタムエレメント用のビルドプラグインもインストール
npm install -D @sveltejs/vite-plugin-svelte

プロジェクト構成

初期構成を以下のように変更します。

my-custom-elements/
├── src/
│   ├── app.d.ts
│   ├── app.html
│   └── lib/
│       ├── assets/
│       │   └── favicon.svg
│       └── components/      # 新規作成
│           ├── index.ts     # 新規作成
│           ├── MyButton.svelte   # 新規作成
│           └── MyCounter.svelte  # 新規作成
├── static/
│   ├── demo.html           # 新規作成
│   ├── demo-dev.html       # 新規作成
│   └── robots.txt
├── vite.lib.config.ts      # 新規作成
├── svelte.config.js
├── tsconfig.json
├── vite.config.ts
└── package.json

まず必要なディレクトリとファイルを作成

# componentsディレクトリを作成
mkdir src/lib/components

ステップ2: カスタムエレメントコンポーネントの作成

カスタムエレメントとして使用するコンポーネントを作成します。

<!-- src/lib/components/MyButton.svelte -->
<svelte:options customElement="my-button" />

<script lang="ts">
  let { label = 'Click me', variant = 'primary' }: {
    label?: string;
    variant?: 'primary' | 'secondary' | 'danger';
  } = $props();

  function handleClick() {
    // ホスト要素(<my-button>)にカスタムイベントをディスパッチ
    $host().dispatchEvent(
      new CustomEvent('boom', {
        detail: {
          message: `Button "${label}" was clicked!`,
          timestamp: Date.now()
        },
        bubbles: true,
        composed: true // Shadow DOMの境界を越えてバブリング
      })
    );

    // ホスト要素にアニメーションクラスを追加
    const host = $host();
    host.classList.add('clicked');
    setTimeout(() => host.classList.remove('clicked'), 300);
  }

  // ホスト要素の初期設定
  $effect(() => {
    const host = $host();
    host.setAttribute('role', 'button');
    host.setAttribute('tabindex', '0');
    host.style.display = 'inline-block';
  });
</script>

<button
  onclick={handleClick}
  class="btn btn-{variant}"
>
  {label}
</button>

<style>
  .btn {
    padding: 10px 20px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 16px;
    transition: all 0.3s ease;
  }

  .btn-primary {
    background: #007bff;
    color: white;
  }

  .btn-primary:hover {
    background: #0056b3;
  }

  .btn-secondary {
    background: #6c757d;
    color: white;
  }

  .btn-danger {
    background: #dc3545;
    color: white;
  }

  :global(.clicked) {
    animation: pulse 0.3s ease;
  }

  @keyframes pulse {
    0% { transform: scale(1); }
    50% { transform: scale(0.95); }
    100% { transform: scale(1); }
  }
</style>

もう一つカウンターコンポーネントも作成

<!-- src/lib/components/MyCounter.svelte(Web Component 化サンプル:上記と同様、
     attribute の初期値スナップショットパターンのため state_referenced_locally 許容) -->
<svelte:options customElement="my-counter" />

<script lang="ts">
  let { initial = 0, step = 1 }: {
    initial?: number | string;
    step?: number | string;
  } = $props();

  // 文字列の場合は数値に変換
  let count = $state(Number(initial));
  let stepValue = Number(step);

  function increment() {
    count += stepValue;
    notifyChange();
  }

  function decrement() {
    count -= stepValue;
    notifyChange();
  }

  function notifyChange() {
    $host().dispatchEvent(
      new CustomEvent('countchange', {
        detail: { count, step: stepValue },
        bubbles: true
      })
    );
  }

  // ホスト要素の初期設定
  $effect(() => {
    const host = $host();
    host.setAttribute('data-count', String(count));
  });
</script>

<div class="counter">
  <button onclick={decrement}>-</button>
  <span class="count">{count}</span>
  <button onclick={increment}>+</button>
</div>

<style>
  .counter {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    padding: 5px;
    border: 1px solid #ccc;
    border-radius: 4px;
  }

  button {
    width: 30px;
    height: 30px;
    border: none;
    background: #007bff;
    color: white;
    border-radius: 4px;
    cursor: pointer;
    font-size: 18px;
  }

  button:hover {
    background: #0056b3;
  }

  .count {
    min-width: 40px;
    text-align: center;
    font-size: 18px;
    font-weight: bold;
  }
</style>

ステップ3: エクスポート設定

コンポーネントをエクスポートするためのエントリーポイントを作成

// src/lib/components/index.ts
// カスタムエレメントの自動登録
import './MyButton.svelte';
import './MyCounter.svelte';

// 必要に応じて追加のコンポーネントをインポート
console.log('Custom elements registered: my-button, my-counter');

export {}; // モジュールとして扱うため

ステップ4: ビルド設定

カスタムエレメント専用のビルド設定ファイルを作成します。

// vite.lib.config.ts (新規作成)
import { svelte } from '@sveltejs/vite-plugin-svelte';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    svelte({
      compilerOptions: {
        customElement: true,
      },
    }),
  ],
  build: {
    lib: {
      entry: 'src/lib/components/index.ts',
      name: 'MyCustomElements',
      // UMDは使用しない(ES moduleのみ)
      formats: ['es'],
      fileName: 'custom-elements',
    },
    rollupOptions: {
      // 必要に応じて外部依存を追加
      external: [],
    },
  },
});
IDEの警告について

VSCodeやWebStormで<svelte:options customElement="...">に対して警告が表示される場合があります。

  • 警告内容: "The customElement option is used when generating a custom element. Did you forget the customElement: true compile option?"
  • 原因: IDE用の設定(svelte.config.js)とビルド用の設定(vite.lib.config.ts)が異なるため
  • 対処: この警告は無視して問題ありません。ビルド時は正常に動作します。

もし警告を消したい場合は、svelte.config.jsに以下を追加

// svelte.config.js (オプション)
export default {
  // ... 既存の設定
  compilerOptions: {
    customElement: true, // IDE警告を消す場合のみ
  },
};

package.jsonにビルドスクリプトを追加

// package.json
{
  "scripts": {
    "dev": "vite dev",
    "build": "vite build",
    "build:lib": "vite build --config vite.lib.config.ts",
    "preview": "vite preview"
  }
}

ステップ5: ビルドとテスト

# カスタムエレメントとしてビルド
npm run build:lib

# 生成されるファイル:
# dist/custom-elements.js    # ES module

ステップ6: テスト用HTMLファイルの作成

開発用とビルド後で異なるHTMLファイルを作成

開発用(推奨)

<!-- static/demo-dev.html -->
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>カスタムエレメントデモ(開発用)</title>
    <!-- 開発サーバーから直接読み込み -->
    <script type="module" src="/src/lib/components/index.ts"></script>
    <style>
      body {
        font-family: Arial, sans-serif;
        padding: 40px;
        max-width: 800px;
        margin: 0 auto;
      }
    </style>
  </head>
  <body>
    <h1>カスタムエレメントデモ</h1>
    <my-button label="テストボタン"></my-button>
    <my-counter initial="5"></my-counter>
  </body>
</html>

ビルド後用

<!-- static/demo.html -->
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>カスタムエレメントデモ</title>
    <!-- ビルド後のファイルを参照 -->
    <script type="module" src="./custom-elements.js"></script>
    <style>
      body {
        font-family: Arial, sans-serif;
        padding: 40px;
        max-width: 800px;
        margin: 0 auto;
      }

      h1 {
        color: #333;
      }

      .demo-section {
        margin: 30px 0;
        padding: 20px;
        border: 1px solid #ddd;
        border-radius: 8px;
      }

      .output {
        margin-top: 20px;
        padding: 10px;
        background: #f0f0f0;
        border-radius: 4px;
        font-family: monospace;
      }
    </style>
  </head>
  <body>
    <h1>Svelteカスタムエレメントデモ</h1>

    <div class="demo-section">
      <h2>ボタンコンポーネント</h2>
      <my-button label="Primary Button" variant="primary"></my-button>
      <my-button label="Secondary" variant="secondary"></my-button>
      <my-button label="Danger!" variant="danger"></my-button>

      <div id="button-output" class="output">
        ボタンをクリックしてください...
      </div>
    </div>

    <div class="demo-section">
      <h2>カウンターコンポーネント</h2>
      <my-counter initial="10" step="5"></my-counter>
      <my-counter initial="0" step="1"></my-counter>

      <div id="counter-output" class="output">
        カウンターの値が変更されると表示されます...
      </div>
    </div>

    <script>
      // カスタムエレメントが定義されるまで待つ
      Promise.all([
        customElements.whenDefined('my-button'),
        customElements.whenDefined('my-counter'),
      ]).then(() => {
        // ボタンのイベントリスナー
        const buttons = document.querySelectorAll('my-button');
        const buttonOutput = document.getElementById('button-output');

        buttons.forEach((btn) => {
          btn.addEventListener('boom', (e) => {
            buttonOutput.textContent = `Event: ${e.detail.message} at ${new Date(e.detail.timestamp).toLocaleTimeString()}`;
          });
        });

        // カウンターのイベントリスナー
        const counters = document.querySelectorAll('my-counter');
        const counterOutput = document.getElementById('counter-output');

        counters.forEach((counter) => {
          counter.addEventListener('countchange', (e) => {
            counterOutput.textContent = `Counter changed: ${e.detail.count} (step: ${e.detail.step})`;
          });
        });
      });
    </script>
  </body>
</html>

ステップ7: 開発サーバーでテスト

方法1: 開発サーバーで直接確認(推奨)

# 開発サーバーを起動
npm run dev

# ブラウザで以下にアクセス
# http://localhost:5173/demo-dev.html

開発サーバーではTypeScriptファイルが直接読み込まれ、ホットリロードも動作します。

動作確認

ブラウザのDevToolsで要素を確認すると、カスタムエレメントが正しく登録されているのが確認できます。

<my-button
  label="テストボタン"
  role="button"
  tabindex="0"
  style="display: inline-block;"
>
  #shadow-root (open)
  <button class="btn btn-primary">テストボタン</button>
</my-button>

カウンターをクリックすると、data-count属性が更新されるのも確認できます。

方法2: ビルド後の確認

# 1. カスタムエレメントをビルド
npm run build:lib

# 2. ビルドしたファイルをstaticディレクトリにコピー
cp dist/custom-elements.js static/

# 3. 開発サーバーを起動
npm run dev

# 4. ブラウザで以下にアクセス
# http://localhost:5173/demo.html
npm run buildとpreviewについて

npm run build はSvelteKitアプリケーション用のビルドコマンドです。

  • .svelte-kit/output/にSvelteKitアプリをビルド
  • カスタムエレメントのビルドには使用しません
  • カスタムエレメントにはnpm run build:libを使用

npm run preview はSvelteKitアプリのプレビュー用です。

  • npm run build後のSvelteKitアプリをプレビュー
  • http://localhost:4173/でアクセス
  • カスタムエレメントのテストには使用しません

カスタムエレメントのテストはnpm run devとdemo-dev.htmlを使用してください。

トラブルシューティング

よくあるエラーと解決方法

  1. “UMD and IIFE output formats are not supported”エラー

    • 原因: SvelteKitのデフォルト設定との競合
    • 解決: 別のvite.lib.config.tsファイルを使用し、formatを['es']のみに設定
  2. カスタムエレメントが登録されない

    • 原因: <svelte:options customElement="...">の記述漏れ
    • 解決: 各コンポーネントファイルの先頭に追加
  3. スタイルが適用されない

    • 原因: Shadow DOMのカプセル化
    • 解決: :global()を使用するか、コンポーネント内でスタイルを定義
  4. $host()が使えない

    • 原因: 通常のSvelteコンポーネントで使用している
    • 解決: <svelte:options customElement="...">を追加
  5. 403 Forbidden エラー(demo.html)

    • 原因: Viteの開発サーバーが/distディレクトリへのアクセスを禁止
    • 解決:
      • 開発時: /src/lib/components/index.tsを直接インポート
      • ビルド後: ビルドファイルをstaticディレクトリにコピー
  6. カスタムエレメントの属性が文字列として扱われる

    • 原因: HTML属性は常に文字列として渡される
    • 例: <my-counter initial="5" step="10"> の5と10は文字列
    • 解決: propsでstring | number型を受け入れ、Number()で変換
    let { step = 1 }: { step?: number | string } = $props();
    let stepValue = Number(step);

NPMパッケージとして配布する場合

package.jsonの設定例:

// package.json (NPMパッケージ用)
{
  "name": "my-svelte-components",
  "version": "1.0.0",
  "type": "module",
  "files": ["dist"],
  "main": "./dist/custom-elements.js",
  "module": "./dist/custom-elements.js",
  "exports": {
    ".": {
      "import": "./dist/custom-elements.js"
    }
  }
}

使用側:

// ESモジュールとしてインポート
import 'my-svelte-components';

// カスタムエレメントが自動的に登録される
// HTMLで<my-button>が使用可能に

$hostでできること

1. カスタムイベントのディスパッチ

<svelte:options customElement="event-emitter" />

<script lang="ts">
  function emitCustomEvent(eventName: string, data: any) {
    $host().dispatchEvent(
      new CustomEvent(eventName, {
        detail: data,
        bubbles: true,     // 親要素へバブリング
        composed: true,    // Shadow DOMを越える
        cancelable: true   // preventDefault()可能
      })
    );
  }
</script>

2. ホスト要素の属性操作

<svelte:options customElement="dynamic-element" />

<script lang="ts">
  import { onMount } from 'svelte';

  let expanded = $state(false);

  $effect(() => {
    // 状態変化に応じてホスト要素の属性を更新
    const host = $host();
    host.setAttribute('aria-expanded', String(expanded));
    host.classList.toggle('expanded', expanded);
  });

  onMount(() => {
    // 初期化時の処理
    const host = $host();
    host.setAttribute('role', 'button');
    host.setAttribute('tabindex', '0');
  });
</script>

<button onclick={() => expanded = !expanded}>
  Toggle
</button>

3. ホスト要素のスタイル制御

<svelte:options customElement="styled-element" />

<script lang="ts">
  let { theme = 'light' } = $props();

  $effect(() => {
    const host = $host();

    // テーマに応じたスタイルを適用
    if (theme === 'dark') {
      host.style.backgroundColor = '#1a1a1a';
      host.style.color = '#ffffff';
    } else {
      host.style.backgroundColor = '#ffffff';
      host.style.color = '#000000';
    }

    // 共通スタイル
    host.style.display = 'block';
    host.style.padding = '20px';
    host.style.borderRadius = '8px';
  });
</script>

4. ホスト要素の情報取得

<svelte:options customElement="info-element" />

<script lang="ts">
  import { onMount } from 'svelte';

  onMount(() => {
    const host = $host();

    // ホスト要素の各種情報を取得
    console.log('タグ名:', host.tagName);           // "INFO-ELEMENT"
    console.log('ID:', host.id);                    // 設定されたID
    console.log('クラス:', host.className);         // 設定されたクラス
    console.log('親要素:', host.parentElement);     // 親要素への参照

    // カスタム属性の取得
    const customAttr = host.getAttribute('data-custom');
    console.log('カスタム属性:', customAttr);
  });
</script>

通常のコンポーネントとの違い

比較表

観点通常のSvelteコンポーネントカスタムエレメント
使用場所Svelteアプリ内のみ任意のHTML/JavaScript環境
Props受け渡しTypeScript型付きpropsHTML属性またはプロパティ
イベントコンポーネントイベントCustomEvent(DOMイベント)
スタイリングスコープ付きCSSShadow DOM(カプセル化)
SSR対応✅ 完全対応❌ 非対応
型安全性✅ TypeScript完全対応⚠️ 限定的
バンドルサイズ最適化されるSvelteランタイム含む
$host使用❌ 使用不可✅ 使用可能

使い分けの指針

// 通常のコンポーネント - Svelteアプリ内で使用
import Button from './Button.svelte';

// カスタムエレメント - 外部環境で使用
<my-button text="Click me"></my-button>
用途推奨
Svelteアプリ内での再利用通常のコンポーネント
他フレームワークとの共有カスタムエレメント
SSR/SSGが必要通常のコンポーネント
WordPressなどCMSへの埋め込みカスタムエレメント
マイクロフロントエンドカスタムエレメント

制限事項と注意点

重要な制限
  • $host()はカスタムエレメント内でのみ使用可能
  • 通常のSvelteコンポーネントで使用するとコンパイルエラー
  • <svelte:options customElement="...">の指定が必須
  • SSR(サーバーサイドレンダリング)は非対応

コンパイル設定

// vite.config.js
export default {
  plugins: [
    svelte({
      compilerOptions: {
        customElement: true, // すべてをカスタムエレメントとしてコンパイル
      },
    }),
  ],
};

または、個別に指定

<!-- 個別のコンポーネントでのみカスタムエレメント化 -->
<svelte:options customElement="my-component" />

まとめ

$hostルーンは、Svelteコンポーネントをカスタムエレメントとして使用する際の重要な機能です。

  • イベント通信: カスタムイベントによる外部との通信
  • 属性制御: ホスト要素の属性やスタイルの動的制御
  • 相互運用性: 他のフレームワークやVanilla JavaScriptとの統合

カスタムエレメントは、コンポーネントの再利用性を最大化し、フレームワークに依存しない形でUIコンポーネントを提供する強力な手段です。

参考資料

公式ドキュメント

MDN Web Docs

次のステップ

$hostの基本を理解したら、次はデバッグに便利な$inspectルーンについて学びましょう。