Vue 3のスロット:デフォルトのスロット、名前付きスロット、スコープ付きスロット
スロットは、Vueのコンテンツ配信APIの一部です。親コンポーネントは、子コンポーネントの「スロット」に任意のコンテンツを挿入できます。スロットにより、コンポーネントは「テンプレート」と同じくらい柔軟になります。つまり、親コンポーネントが表示内容を制御し、子コンポーネントが表示位置を制御する仕組みです。
Vue 3 では、デフォルトスロット、名前付きスロット、スコープ付きスロットの 3 種類のスロットが用意されています。このレッスンでは、これら 3 種類のスロットの使い方とベストプラクティスを習得できるよう解説します。
1. 学習内容
- デフォルトスロット
<slot />の基本的な使い方 <slot name="header" />およびv-slot:headerという名称のスロットの略称- スコープスロット:子コンポーネントは、親コンポーネントのスロットにデータを渡します
<template v-slot>構文 (Vue 2.6+)- 動的なスロット名と略語の構文
- デフォルトのスロットコンテンツ(フォールバックコンテンツ)
$slots/useSlotsフック
2. カードコンポーネントの内容をカスタマイズできないという課題
(1) 課題:カードスタイルが5種類あり、5つのコンポーネントを作成する必要がある
アリスのEコマース管理担当者は、5種類のカードを必要としていました:
VUE
<!-- ❌ The "Flip" Version:5 component -->
<!-- ProductCard.vue -->
<div class="card">{{ product.name }}</div>
<!-- UserCard.vue -->
<div class="card">{{ user.name }}</div>
<!-- OrderCard.vue -->
<div class="card">Order #{{ order.id }}</div>
<!-- 5 component,90% The code is the same,The only difference is the content -->
プロダクトマネージャーのチャーリー:
「アリス、来週はあと5種類のカードが必要なんだ。これ以上新しいコンポーネントを追加し続けるわけにはいかない。どんなコンテンツでも対応できる、柔軟性のあるカードが必要なんだ。」
(2) Vueのスロットを用いた解決策:1つのBaseCardで、コンテンツは親コンポーネントから提供される
VUE
<!-- BaseCard.vue - General-Purpose Card,The slot accepts any content -->
<template>
<div class="card">
<div v-if="$slots.header" class="card-header">
<slot name="header" />
</div>
<div class="card-body">
<slot /> <!-- Default Slot -->
</div>
<div v-if="$slots.footer" class="card-footer">
<slot name="footer" />
</div>
</div>
</template>
VUE
<!-- ProductCard Usage BaseCard -->
<BaseCard>
<template #header>
<h3>Product</h3>
</template>
<p>iPhone 15 Pro</p> <!-- Default Slot -->
<template #footer>
<button>Add to Cart</button>
</template>
</BaseCard>
1 BaseCard.vue → 無限の種類のカード。新しいカードを追加するたびに、コンテンツを記述するだけで済み、コンポーネントを変更する必要はありません。
(3) 収益
スロットを使用した後は:
- コンポーネント数:5 → 1(-80%)
- 新しいカードタイプ:テンプレートコード 1 行
- メンテナンスコスト:1つのスタイルを変更するだけで、5枚以上のカードが同時に更新される
- 再利用性:BaseCardは、カードに関連するあらゆる場面で利用できます
3. デフォルトのスロット
(1) 基本的な使い方
VUE
<!-- BaseCard.vue Child component -->
<template>
<div class="card">
<!-- Slot: The parent component can contain any content -->
<slot />
</div>
</template>
VUE
<!-- App.vue Parent Component -->
<template>
<BaseCard>
<h3>Hello</h3>
<p>This is the card content</p>
</BaseCard>
</template>
レンダリング結果:
HTML
<div class="card">
<h3>Hello</h3>
<p>This is the card content</p>
</div>
(2) デフォルトのコンテンツ(フォールバック)
VUE
<!-- Child component -->
<template>
<div class="card">
<slot>
<!-- Default Content:Display when the parent component does not pass data -->
<p>No content provided</p>
</slot>
</div>
</template>
VUE
<!-- The parent component does not pass content -->
<BaseCard />
<!-- Rendering:<p>No content provided</p> -->
<!-- Passing Content from the Parent Component -->
<BaseCard>
<p>Custom content</p>
</BaseCard>
<!-- Rendering:<p>Custom content</p>(Override Default)-->
(3) 5つの主なユースケース
| シナリオ | 用途 |
|---|---|
| カードの内容 | <slot /> |
| ボタンのテキスト | <slot /> |
| リスト項目 | <slot :item="item" /> |
| フォームフィールド | <slot /> |
| モーダルボックスの本文 | <slot /> |
4. 名前付きスロット
(1) 名前付きスロットの定義
VUE
<!-- BaseLayout.vue Child component -->
<template>
<div class="layout">
<header>
<slot name="header" />
</header>
<main>
<slot /> <!-- Default Slot(Optional name="default") -->
</main>
<footer>
<slot name="footer" />
</footer>
</div>
</template>
(2) 親コンポーネントの使用(3つの構文)
VUE
<!-- App.vue Parent Component -->
<!-- Writing Style 1:v-slot:name(Most Comprehensive) -->
<BaseLayout>
<template v-slot:header>
<h1>My App</h1>
</template>
<template v-slot:default>
<p>Main content</p>
</template>
<template v-slot:footer>
<p>Footer</p>
</template>
</BaseLayout>
<!-- Writing Style 2:Abbreviation #name(Recommendations) -->
<BaseLayout>
<template #header>
<h1>My App</h1>
</template>
<p>Main content</p> <!-- The default slot can be omitted template -->
<template #footer>
<p>Footer</p>
</template>
</BaseLayout>
<!-- Writing Style 3:v-slot Receive multiple slot objects(Vue 3 Recommendations) -->
<BaseLayout>
<template #header>
<h1>My App</h1>
</template>
<template #default="{ user }"> <!-- Deconstructing Scope Slots -->
<p>Hello, {{ user.name }}</p>
</template>
<template #footer>
<button>Logout</button>
</template>
</BaseLayout>
(3) 動的なスロット名
VUE
<!-- Child component -->
<template>
<div>
<slot :name="dynamicSlotName" />
</div>
</template>
<script setup>
import { ref } from 'vue'
const dynamicSlotName = ref('header')
</script>
<!-- Parent Component:Dynamic Slot Binding -->
<BaseLayout>
<template #[dynamicSlotName]>
<p>Dynamic content</p>
</template>
</BaseLayout>
(4) スロットの有無を確認する
VUE
<!-- Child component:Check whether data is being received through the slot -->
<template>
<div class="card">
<div v-if="$slots.header" class="card-header">
<slot name="header" />
</div>
<slot />
</div>
</template>
VUE
<!-- Parent Component -->
<BaseCard>
<template #header>...</template> <!-- Sent header Slot, Display -->
</BaseCard>
<BaseCard>
<!-- No header slot passed, Do not display card-header -->
</BaseCard>
5. スコープスロット
(1) 要点:子コンポーネントは、親コンポーネントのスロットにデータを渡す
VUE
<!-- Child component:UserList.vue -->
<template>
<ul>
<li v-for="user in users" :key="user.id">
<!-- Pass user to the parent component's slot -->
<slot :user="user" :index="index" />
</li>
</ul>
</template>
<script setup>
defineProps({ users: Array })
</script>
VUE
<!-- Parent Component:App.vue -->
<template>
<UserList :users="users">
<!-- Receive data passed from child components -->
<template #default="{ user, index }">
<p>{{ index + 1 }}. {{ user.name }} ({{ user.email }})</p>
</template>
</UserList>
</template>
レンダリング結果:
HTML
<ul>
<li><p>1. Alice (alice@example.com)</p></li>
<li><p>2. Bob (bob@example.com)</p></li>
<li><p>3. Charlie (charlie@example.com)</p></li>
</ul>
(2) 5つのスコープスロットの配置パターン
| パターン | 親コンポーネントの実装 | 目的 |
|---|---|---|
| すべてのプロップを受け入れる | <template #default="slotProps"> |
オブジェクト全体を受け入れる |
| 脱構築 | <template #default="{ user }"> |
必要なフィールドのみを使用する |
| 名前の変更 | <template #default="{ user: u }"> |
変数の競合を避ける |
| デフォルト値 | <template #default="{ user = defaultUser }"> |
デコンストラクション時にデフォルト値を指定する |
| テンプレートなし | {{ slotProps.user.name }} |
シンプルなシナリオ(デフォルトのスロット) |
(3) 完全な例:カスタマイズ可能なリスト
VUE
<!-- UserList.vue Child component -->
<template>
<ul class="user-list">
<li v-for="(user, index) in users" :key="user.id">
<slot :user="user" :index="index" :isAdmin="user.role === 'admin'" />
</li>
</ul>
</template>
<script setup>
defineProps({ users: Array })
</script>
VUE
<!-- Using Parent Components(3 One way) -->
<template>
<!-- Method 1:A Brief Overview -->
<UserList :users="users">
<template #default="{ user }">
{{ user.name }}
</template>
</UserList>
<!-- Method 2:Table Display -->
<UserList :users="users">
<template #default="{ user, index, isAdmin }">
<tr>
<td>{{ index + 1 }}</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>
<span v-if="isAdmin" class="badge admin">Admin</span>
<span v-else class="badge user">User</span>
</td>
</tr>
</template>
</UserList>
<!-- Method 3:Card Display -->
<UserList :users="users">
<template #default="{ user }">
<UserCard :user="user" />
</template>
</UserList>
</template>
6. $slots と useSlots
(1) $slots スロットへのアクセス
VUE
<!-- Child component -->
<template>
<div>
<!-- List all incoming slot names -->
<div v-for="(_, name) in $slots" :key="name">
<slot :name="name" />
</div>
</div>
</template>
(2) useSlots(Composition API)
VUE
<script setup>
import { useSlots } from 'vue'
const slots = useSlots()
// Check if the slot exists
if (slots.header) {
console.log('Header slot exists')
}
// Dynamic Rendering
if (slots.default) {
console.log('Default slot exists')
}
</script>
(3) $slots 対 useSlots
| 寸法 | $slots |
useSlots() |
|---|---|---|
| APIの種類 | テンプレート内で直接使用 | JS(Composition API)で使用 |
| 戻る | スロットオブジェクト | スロットオブジェクト(refs形式) |
| シーン | テンプレート内 | <script setup> 内 |
7. 完全な例:カスタマイズ可能なカードコンポーネント
(1) ▶ サンプル:. BaseCard.vue(デフォルト+名前付きスロット)
VUE
<!-- src/components/BaseCard.vue -->
<template>
<div class="card">
<div v-if="$slots.header" class="card-header">
<slot name="header" />
</div>
<div class="card-body">
<slot>
<!-- Default Content:Display when the parent component does not pass data -->
<p>No content</p>
</slot>
</div>
<div v-if="$slots.footer" class="card-footer">
<slot name="footer" />
</div>
</div>
</template>
<style scoped>
.card {
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
}
.card-header, .card-footer {
background: #f9fafb;
padding: 1rem;
}
.card-body {
padding: 1rem;
}
</style>
(2) ▶ サンプル:. BaseCardの5つの活用方法
VUE
<!-- 1. The simplest(Transmit Only body) -->
<BaseCard>
<p>Simple content</p>
</BaseCard>
<!-- 2. With header -->
<BaseCard>
<template #header>
<h3>Product Name</h3>
</template>
<p>Product description</p>
</BaseCard>
<!-- 3. Complete header + body + footer -->
<BaseCard>
<template #header>
<h3>Order #12345</h3>
</template>
<p>Total: $99.99</p>
<template #footer>
<button>View Details</button>
</template>
</BaseCard>
<!-- 4. Complete Syntax(v-slot) -->
<BaseCard>
<template v-slot:header>
<h3>Header</h3>
</template>
<template v-slot:default>
<p>Body</p>
</template>
<template v-slot:footer>
<p>Footer</p>
</template>
</BaseCard>
<!-- 5. Scope Slot(Receive Data) -->
<UserCard :user="user">
<template #actions="{ user }">
<button @click="edit(user)">Edit</button>
<button @click="remove(user)">Delete</button>
</template>
</UserCard>
(3) ▶ サンプル:. スコープスロットの完全な例
VUE
<!-- Child component:DataTable.vue -->
<template>
<table>
<thead>
<tr>
<th v-for="col in columns" :key="col.key">{{ col.label }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in data" :key="row.id">
<td v-for="col in columns" :key="col.key">
<!-- Scope Slot:Allow the parent component to customize cell rendering -->
<slot :name="`cell-${col.key}`" :row="row" :index="index" :value="row[col.key]">
<!-- Default:Show Original Values -->
{{ row[col.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
</template>
<script setup>
defineProps({
columns: { type: Array, required: true },
data: { type: Array, required: true }
})
</script>
VUE
<!-- Using Parent Components DataTable -->
<DataTable :columns="columns" :data="users">
<!-- Custom status column -->
<template #cell-status="{ row }">
<span :class="['badge', row.status]">{{ row.status }}</span>
</template>
<!-- Custom actions column -->
<template #cell-actions="{ row }">
<button @click="edit(row)">Edit</button>
</template>
</DataTable>
(4) ▶ サンプル:. よくある5つの間違いのクイックリファレンス
| エラー | 症状 | 解決策 |
|---|---|---|
| スロット名のスペルミス | コンテンツが表示されない | <slot name="..."> と #name が一致しているか確認してください |
| 親コンポーネント内に複数のルート要素が存在します | 警告 | 親コンポーネントが <template #name> で囲まれています |
| v-slot(旧構文) | Vue 2 対応 | 代わりに v-slot:header または #header を使用してください |
| デフォルトのスロットが渡されていない | 親コンポーネントが表示されていない | 親コンポーネント内の <Component /> の内容を確認してください |
| スコープスロットはデストラクチャリングされない | テンプレートは冗長である | { user, index } によるデストラクチャリング |
(5) ▶ サンプル:. 5 性能比較
| 構文 | コンパイル | パフォーマンス | 推奨事項 |
|---|---|---|---|
<slot /> |
静的 | ⭐⭐⭐⭐⭐ | デフォルト |
<slot name="x" /> |
静的 | ⭐⭐⭐⭐⭐ | 命名済み |
<slot :x="x" /> |
静的 | ⭐⭐⭐⭐ | スコープ |
v-if="$slots.x" |
状態 | ⭐⭐⭐⭐ | 検出済み |
useSlots() |
実行時間 | ⭐⭐⭐ | JSで使用 |
(6) ▶ サンプル:. $slots の主な5つの用途
VUE
<script setup>
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
</script>
<template>
<div class="wrapper">
<!-- 1. Check if the slot exists -->
<div v-if="slots.header">Has header</div>
<!-- 2. List all slot names -->
<div v-for="name in Object.keys(slots)" :key="name">
<slot :name="name" />
</div>
<!-- 3. Passthrough attribute -->
<input v-bind="attrs" />
<!-- 4. Conditional Rendering -->
<template v-if="slots.default">
<div class="content">
<slot />
</div>
</template>
<!-- 5. Default Slot Packaging -->
<div class="default-wrapper">
<slot>
<p>Default fallback</p>
</slot>
</div>
</div>
</template>
❓ よくある質問
Q スロットとプロップの違いは何ですか?
A プロップはデータ(任意の JavaScript 値)を渡すのに対し、スロットはコンテンツ(HTML やコンポーネントの断片)を渡します。プロップは設定オプションに適しており、スロットはコンテンツのカスタマイズに適しています。
Q スコープスロットはどのような場合に使用すべきですか?
A 親コンポーネントが、子コンポーネントからのデータに基づいてレンダリングをカスタマイズする必要がある場合です。例えば、テーブルの列のレンダリング、リスト項目のレンダリング、またはカードコンテンツのレイアウトなどです。
Q
v-slot と # の省略形にはどのような違いがありますか?A これらは完全に同等です。
#header is a shorthand for v-slot:header。省略形の使用をお勧めします(より簡潔だからです)。Q デフォルトのスロットに名前を付けることはできますか?
A デフォルトのスロットに名前を付ける必要はありません。デフォルトは
<slot /> です。どうしても名前を付けなければならない場合は、<slot name="default" /> を使用してください。親コンポーネントでは <template #default> を使用するか、単にコンテンツを直接渡すことができます。Q スロットをプロップとして渡すことはできますか?
A スロットを直接プロップとして渡すことはできません。ただし、VNode やレンダリング関数をプロップとして渡すことは可能です(上級者向け)。ほとんどの場合は、単に
<slot> を使用してください。Q 親コンポーネント内で子コンポーネントのスロットを検出するにはどうすればよいですか?
A
$slots(テンプレート)または useSlots()(JS)を使用します。子コンポーネントは <slot> を使用してスロットを公開し、親コンポーネントは <template #name> を使用してそのスロットに値を設定します。Q 名前付きスロットにデフォルトの内容を設定できますか?
A はい。
<slot name="header"><h3>Default Header</h3></slot>。これは、親コンポーネントがヘッダーを渡さない場合に表示されます。📖 まとめ
- スロットは Vue のコンテンツ配置 API です。親コンポーネントがコンテンツを制御し、子コンポーネントが配置を制御します。
- 3種類のスロット:デフォルトスロット(
<slot />)、名前付きスロット(name="x")、スコープ付きスロット(:x="x") - 親コンポーネントは、
<template #name>またはその略称である#nameを使用して、名前付きスロットを渡します。 - スコープスロットを使用すると、子コンポーネントから親コンポーネントへデータを渡すことができます(スロットプロパティ)。
<slot>親コンポーネントからコンテンツが渡されない場合に表示されるデフォルトのコンテンツ(フォールバック)$slots/useSlots()スロットが存在するか確認する- スロットとプロップの扱い:プロップは設定を渡し、スロットはコンテンツを渡す
📝 練習問題
(1) 基礎問題(難易度:⭐)
簡単なButtonコンポーネントを実装します:
- BaseButton.vue:デフォルトのスロット + 3つのバリエーション(primary/success/danger)
- props:
variant(文字列) - 親コンポーネント:3つのバリエーションとカスタムスロットコンテンツをテスト
(2) 上級問題(難易度:⭐⭐)
BaseLayout コンポーネントを実装します:
- 子コンポーネント:3つの名前付きスロット(ヘッダー/デフォルト/フッター)
- 親コンポーネント:ダッシュボードページを実装するには、BaseLayout を使用してください
$slotsを使用して、スロットが存在するかどうかを確認してください- デフォルトのコンテンツ(フォールバック)を追加する
(3) チャレンジ問題(難易度:⭐⭐⭐)
完全なDataTableコンポーネントシステムを実装する:
- DataTable.vue:列とデータを受け取り、テーブルを表示する
- スコープスロット:
#cell-{key}親コンポーネントがセルをカスタマイズできるようにします - デフォルトのスロット:親コンポーネントが値を指定していない場合に、元の値を表示します
- 親コンポーネントの例:ユーザーテーブルと注文テーブル――2つの異なる列のレンダリング方法
- パフォーマンス:v-memoは行をキャッシュします(データに変更がない場合は再描画をスキップします)
- TypeScriptの厳格な型付け



