$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> 他フレームワークでの使用例
// 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 -->
<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: [],
}
}
}); 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 は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を使用してください。
トラブルシューティング
よくあるエラーと解決方法
"UMD and IIFE output formats are not supported"エラー
- 原因: SvelteKitのデフォルト設定との競合
- 解決: 別のvite.lib.config.tsファイルを使用し、formatを
['es']のみに設定
カスタムエレメントが登録されない
- 原因:
<svelte:options customElement="...">の記述漏れ - 解決: 各コンポーネントファイルの先頭に追加
- 原因:
スタイルが適用されない
- 原因: Shadow DOMのカプセル化
- 解決:
:global()を使用するか、コンポーネント内でスタイルを定義
$host()が使えない- 原因: 通常のSvelteコンポーネントで使用している
- 解決:
<svelte:options customElement="...">を追加
403 Forbidden エラー(demo.html)
- 原因: Viteの開発サーバーが
/distディレクトリへのアクセスを禁止 - 解決:
- 開発時:
/src/lib/components/index.tsを直接インポート - ビルド後: ビルドファイルを
staticディレクトリにコピー
- 開発時:
- 原因: Viteの開発サーバーが
カスタムエレメントの属性が文字列として扱われる
- 原因: HTML属性は常に文字列として渡される
- 例:
<my-counter initial="5" step="10">の5と10は文字列 - 解決: 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"
}
}
} 使用側:
// 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型付きprops | HTML属性またはプロパティ |
| イベント | コンポーネントイベント | CustomEvent(DOMイベント) |
| スタイリング | スコープ付きCSS | Shadow 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コンポーネントを提供する強力な手段です。
参考資料
公式ドキュメント
- 🔗 Custom elements API - Svelte 5
- 🔗 $host - Svelte 5 Docs
- 🔗 Compiling to custom elements - Svelte Tutorial
MDN Web Docs
次のステップ
$hostの基本を理解したら、次はデバッグに便利な $inspect ルーンについて学びましょう。