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

$hostルーンは、Svelte 5で導入された、カスタムエレメント(Web Components)内でホスト要素にアクセスするための特別なルーンです。

カスタムエレメントとは

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

さらに詳しく学ぶ

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

MDN Web Docs

TypeScriptでのWebComponentsの学習リソース

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

<!-- Counter.svelte -->
<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>
svelte

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

// 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' });
}
javascript
<!-- 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>
html

実践例

$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
bash

プロジェクト構成

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

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
null

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

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

ステップ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>
svelte

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

<!-- src/lib/components/MyCounter.svelte -->
<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>
svelte

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

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

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

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

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

ステップ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: [],
    }
  }
});
typescript
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警告を消す場合のみ
  }
};
javascript

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

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

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

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

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

ステップ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>
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>
html

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

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

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

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

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

動作確認

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

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

カウンターをクリックすると、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
bash
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 devdemo-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">510は文字列
    • 解決: propsでstring | number型を受け入れ、Number()で変換
    let { step = 1 }: { step?: number | string } = $props();
    let stepValue = Number(step);
    typescript

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"
    }
  }
}
javascript

使用側:

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

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

$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>
svelte

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>
svelte

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>
svelte

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コンポーネントカスタムエレメント
使用場所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>
typescript
用途推奨
Svelteアプリ内での再利用通常のコンポーネント
他フレームワークとの共有カスタムエレメント
SSR/SSGが必要通常のコンポーネント
WordPressなどCMSへの埋め込みカスタムエレメント
マイクロフロントエンドカスタムエレメント

制限事項と注意点

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

コンパイル設定

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

または、個別に指定

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

まとめ

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

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

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

参考資料

公式ドキュメント

MDN Web Docs

次のステップ

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

Last update at: 2025/08/31 13:56:47