Vue — Slot
テクノロジ系 / L02 component-design — コンポーネント設計
Vue — Slot
デフォルト slot
<!-- Card.vue -->
<template>
<div class="card">
<slot></slot>
</div>
</template><!-- 使う側 --> <Card> <h2>Title</h2> <p>Content</p> </Card>
<slot></slot> の位置に親が渡した中身が入る。
React の children に相当。
デフォルト値
<!-- Card.vue -->
<template>
<div class="card">
<slot>Default content</slot>
</div>
</template>→ slot が空のときは "Default content" が出る。
Named Slot
複数の slot 位置を用意:
<!-- Layout.vue -->
<template>
<div class="layout">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template><!-- 使う側 -->
<Layout>
<template #header>
<h1>Site Title</h1>
</template>
<p>Main content</p> <!-- default slot -->
<template #footer>
<p>© 2026</p>
</template>
</Layout>#name (= v-slot:name) で対象を指定。
Scoped Slot — slot に値を渡す
子から親に値を渡したい:
<!-- ItemList.vue -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot :item="item" :index="i" />
</li>
</ul>
</template><!-- 使う側 -->
<ItemList :items="users">
<template #default="{ item, index }">
{{ index + 1 }}. {{ item.name }}
</template>
</ItemList>React の Render Props に相当。
子は構造を担当、親は見た目を決める 分業。
動的 slot 名
<template>
<BaseLayout>
<template #[slotName]>
Content
</template>
</BaseLayout>
</template>#[slotName] で動的指定。
高度パターン、普段は使わない。
React との対比
| React | Vue | |
|---|---|---|
| 中身渡し | children | default slot |
| 複数slot | 複数 props | named slot |
| 値を渡す | Render Props (関数) | Scoped Slot |
Vue の slot は 宣言的、React は 関数渡し。
読みやすさはほぼ同等。
まとめ
<slot></slot>でデフォルト中身渡し<slot name="x">+<template #x>で複数 slot- Scoped Slot で子から値を渡す
- 動的 slot 名は
#[name] - React の children / Render Props に対応
最後は provide / inject。
考えてみよう
既定では正解・不正解の判定はしません。自分のペースで「答えを見る」を。採点してほしい場合だけ「採点してほしい」を押してください。
Lv1: 基本 — デフォルト slot
問題
<!-- Card.vue -->
<template>
<div class="card">
<slot>Default content</slot>
</div>
</template>
<!-- 親 -->
<Card>
<h2>Title</h2>
<p>Body</p>
</Card>
<Card /> <!-- slot 空 → "Default content" 表示 -->合格基準
- slot に複数要素を渡せる
- 空のときデフォルト
Lv2: 応用 — Named slot
問題
<!-- Layout.vue -->
<template>
<div class="layout">
<header><slot name="header" /></header>
<main><slot /></main>
<aside><slot name="sidebar" /></aside>
<footer><slot name="footer" /></footer>
</div>
</template>
<!-- 親 -->
<Layout>
<template #header><h1>Site</h1></template>
<template #sidebar><nav>Menu</nav></template>
<p>Main content</p>
<template #footer><p>© 2026</p></template>
</Layout>合格基準
- 4 slot が指定位置に描画
#nameshorthand 使用
Lv3: 発展 — Scoped Slot
問題
<!-- ItemList.vue -->
<script setup lang="ts">
const props = defineProps<{ items: { id: number; name: string }[] }>();
</script>
<template>
<ul>
<li v-for="(item, i) in props.items" :key="item.id">
<slot :item="item" :index="i" />
</li>
</ul>
</template>
<!-- 親 -->
<ItemList :items="users">
<template #default="{ item, index }">
{{ index + 1 }}. {{ item.name }} (#{{ item.id }})
</template>
</ItemList>子が構造、親が見た目を決めるパターン。
合格基準
- scoped slot で子の値を親が使う
- ループの index + item が表示
★発展: 構造の再利用とスタイルの分離
題材『ニュース記事カード一覧』。
named slot で 構造 は再利用できる。だが スタイル は slot では巻き取れない —
別の仕組み(CSS変数 + scoped class)が要る。この分離原則を体で覚える。
◆Step A: named slot でカードを組む(既出の復習)
枠だけ担う BaseCard(無名 slot)と、領域を切った ArticleCard(named slot)の2層にする。
<!-- BaseCard.vue — 枠(角丸の箱)だけ提供。中身は知らない -->
<template>
<div class="base-card">
<slot />
</div>
</template><!-- ArticleCard.vue — thumbnail / title / meta の3領域を切る -->
<template>
<BaseCard>
<div class="thumb"><slot name="thumbnail" /></div>
<h3 class="title"><slot name="title" /></h3>
<div class="meta"><slot name="meta" /></div>
</BaseCard>
</template><!-- 使う側 — 記事ごとに #title などで注入 -->
<ArticleCard v-for="a in articles" :key="a.id">
<template #thumbnail><img :src="a.image" :alt="a.title" /></template>
<template #title>{{ a.title }}</template>
<template #meta>{{ a.author }} ・ {{ a.date }}</template>
</ArticleCard>合格基準
BaseCardは中身を一切知らず、<slot />で受け取るだけArticleCardの3つの named slot がそれぞれ指定位置に描画される- 呼び出し側が
<template #title>等で記事データを注入できる
◆Step B: 装飾をインライン style にベタ書きする(痛みを作る)
まず わざと 角丸とアクセント色を全カードのインライン style に直書きする。
一覧には 5〜6 枚のカードを並べること。
<!-- ArticleCard.vue — 全部インラインに直書き(このあと回収する) -->
<template>
<div
:style="{
borderRadius: '8px',
borderLeft: '4px solid #3b82f6',
}"
>
<div class="thumb"><slot name="thumbnail" /></div>
<h3
class="title"
:style="{ color: '#3b82f6' }"
><slot name="title" /></h3>
<div class="meta"><slot name="meta" /></div>
</div>
</template>ここで体感すること
- アクセント色を
#3b82f6→ 別の色に変えたい - 角丸を
8px→12pxにしたい - → インラインだと 全カード・全箇所を手で書き換える ことになる
- slot で構造は1箇所に集約できたのに、スタイルは集約できていない
合格基準
- 一覧に 5 枚以上のカードが並んでいる
- 色・角丸がインライン style に散らばっている状態を再現できている
- 「色を1つ変えるのに何箇所触るか」を数えて記録する(後で比較する)
◆Step C: CSS変数 + scoped class に巻き取る(負債回収)
散らばった装飾を :root の CSS変数と scoped class に集約する。
インライン style は全廃する。
/* グローバル(main.css など) */
:root {
--card-radius: 8px;
--accent-color: #3b82f6;
}<!-- ArticleCard.vue — 装飾は scoped class へ。変数を参照する -->
<template>
<div class="article-card">
<div class="thumb"><slot name="thumbnail" /></div>
<h3 class="title"><slot name="title" /></h3>
<div class="meta"><slot name="meta" /></div>
</div>
</template>
<style scoped>
.article-card {
border-radius: var(--card-radius);
border-left: 4px solid var(--accent-color);
}
.title {
color: var(--accent-color);
}
</style>合格基準(観測可能)
:rootの--accent-colorを 1行変えるだけ で、一覧の全カードのアクセント色(左罫線+タイトル色)が一斉に変わる--card-radiusを 1行変えるだけで全カードの角丸が変わるArticleCard.vueに:style=のインライン直書きが 1つも残っていない- 構造(slot)と装飾(CSS変数 + scoped class)が別の仕組みで管理されている
> 学びの核: slot は「何を差し込むか(構造)」の再利用。スタイルは別レイヤ(CSS変数)で再利用する。
> 構造の容器とスタイルのテーマは混ぜず、それぞれの仕組みに分けて持つ。
提出
submission.md に Lv1〜Lv3 の回答と、★発展の Step A〜C の成果(特に Step B→C の前後で「色を変えるのに触る箇所が何箇所→1箇所」になったか)を。
ヒント
<slot />は「親がタグの中に書いた内容」が入る穴(デフォルト slot)。<slot name="header" />は「親が#headerで差し込んだ内容」が入る穴(名前付き slot)。- 親側はこう使う:
<Card>
<template #header>タイトル</template>
本文(デフォルト slot に入る)
</Card>
- props は「値」を渡すが、slot は「テンプレート(HTML の塊)」を渡せるのが違い。
解説
slot は「テンプレートを差し込む穴」
props は 値(文字列・数値・オブジェクト)を渡す。slot は テンプレート(HTML の塊) を渡せる。
カードの「枠」(角丸・ヘッダの背景・余白)は共通だが、中身は場所ごとに違う。
中身を props で全部受け取るのは無理(見出し・本文・ボタン…構造が毎回違う)。
そこで「枠だけ作って、中身は親に書かせる」のが slot。
<!-- 子: Card.vue --> <div class="card"> <div class="card-header"><slot name="header" /></div> <!-- 名前付き --> <div class="card-body"><slot /></div> <!-- デフォルト --> </div>
<!-- 親: 使う側 --> <Card> <template #header>ユーザー情報</template> <!-- header slot に入る --> <p>名前: 山田</p> <!-- デフォルト slot に入る --> </Card>
<slot />(デフォルト)… 親がタグの中に直接書いた内容。<slot name="header" />(名前付き)… 親が<template #header>で指定した内容。- 名前で振り分くので、ヘッダと本文が混ざらない。
props と slot の使い分け
| 渡したいもの | 使うもの |
|---|---|
| 値(タイトル文字列・件数・フラグ) | props |
| 構造を持つ中身(任意の HTML・別コンポーネント) | slot |
Layout(共通の枠)、Modal(中身が毎回違うダイアログ)、Card のような
「外枠は共通・中身は呼び出し側が決める」部品は slot で作る。
実アプリの再利用部品は props と slot を組み合わせて設計する。