Vue.js: Slots in Vue 3
Last updated: 2026-10-20
Slots are Vue's content distribution API — a parent component can insert arbitrary content into a child component's "slot". Slots make a component as flexible as a "template": the parent decides what is shown, the child decides where it is shown.
Vue 3 provides 3 kinds of slots: default slots, named slots, and scoped slots. This lesson covers all three and the best practices around them.
1. What You'll Learn
- Basic usage of the default slot
<slot /> - Named slots
<slot name="header" />and thev-slot:headershorthand - Scoped slots: the child passes data to the parent's slot content
- The
<template v-slot>syntax (Vue 2.6+) - Dynamic slot names and the shorthand syntax
- Default slot content (fallback content)
- The
$slots/useSlotshook
2. The Challenge of a Card Component That Cannot Be Customized
(1) Pain Point: 5 Card Styles Means 5 Components
Alice's e-commerce admin needs 5 kinds of cards:
<!-- ❌ Broken version: 5 components -->
<!-- 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 components, 90% identical code, only the content differs -->
Product manager Charlie:
"Alice, next week we're adding 5 more card types. We can't keep adding new components. We need a flexible Card that accepts any content."
(2) The Vue Slot Solution: 1 BaseCard, the Parent Inserts Content
1 BaseCard.vue → unlimited card types. Each new card only needs content; the component never changes.
(3) Benefits
After adopting slots:
- Number of components: 5 → 1 (-80%)
- New card types: 1 line of template code
- Maintenance cost: change the style in 1 place, 5+ cards update together
- Reusability: BaseCard works for every card scenario
3. Default Slots
(1) Basic Usage
Rendered result:
<div class="card">
<h3>Hello</h3>
<p>This is the card content</p>
</div>
(2) Default Content (fallback)
<!-- The parent passes no content -->
<BaseCard />
<!-- Renders: <p>No content provided</p> -->
<!-- The parent passes content -->
<BaseCard>
<p>Custom content</p>
</BaseCard>
<!-- Renders: <p>Custom content</p> (overrides the default) -->
(3) 5 Use Cases
| Scenario | Usage |
|---|---|
| Card content | <slot /> |
| Button label | <slot /> |
| List item | <slot :item="item" /> |
| Form field | <slot /> |
| Modal body | <slot /> |
4. Named Slots
(1) Defining Named Slots
(2) Using Them in the Parent (3 Syntaxes)
(3) Dynamic Slot Names
(4) Detecting Whether a Slot Exists
5. Scoped Slots
(1) Core Idea: the Child Passes Data to the Parent's Slot
Rendered result:
<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 Scoped Slot Patterns
| Pattern | Parent syntax | Purpose |
|---|---|---|
| Receive all props | <template #default="slotProps"> |
Receive the whole object |
| Destructure | <template #default="{ user }"> |
Use only the fields you need |
| Rename | <template #default="{ user: u }"> |
Avoid variable clashes |
| Default values | <template #default="{ user = defaultUser }"> |
Provide a default while destructuring |
| No template | {{ slotProps.user.name }} |
Simple cases (default slot) |
(3) Complete Example: A Customizable List
▶ Example: Scoped Slots (Custom DataTable Columns)
▶ Expand runnable example (71 lines)
Output:
A 5-column table (ID / Name / Email / Status / Actions) with two rows.
The status column renders coloured badges — green active for Alice, grey inactive for Bob — and the actions column renders an Edit button per row, while ID / Name / Email fall back to raw values because the parent defines no slot for them.
ID | Name | Email | Status | Actions
1 | Alice | alice@x.com | [active] | [Edit]
2 | Bob | bob@x.com | [inactive] | [Edit]
(1) 📊 5 BaseCard Usage Patterns (Reference Only)
| Pattern | Parent template | Purpose |
|---|---|---|
| 1. Simplest (body only) | <BaseCard><p>Simple</p></BaseCard> |
Default slot only |
| 2. With a header | <BaseCard><template #header>...</template>...</BaseCard> |
Add a header |
| 3. Full header + body + footer | three <template #name> blocks |
Full layout |
| 4. Explicit form (v-slot) | <template v-slot:header> |
Long form (legacy docs) |
| 5. Scoped slot | <template #actions="{ user }"> |
Child → parent data |
(2) 📊 5 Common Mistakes (Reference Only)
| Mistake | Symptom | Fix |
|---|---|---|
| Misspelled slot name | Content does not show | Make <slot name="..."> and #name match |
| Multiple root elements in the parent | Warning | Wrap them in <template #name> |
| Legacy v-slot syntax | Vue 2 compatibility | Switch to v-slot:header or #header |
| Default slot never passed | Nothing shows | Check the content inside <Component /> |
| Not destructuring a scoped slot | Verbose templates | Destructure with { user, index } |
(3) 📊 5 Performance Comparisons
| Syntax | Compilation | Performance | Recommendation |
|---|---|---|---|
<slot /> |
Static | ⭐⭐⭐⭐⭐ | Default |
<slot name="x" /> |
Static | ⭐⭐⭐⭐⭐ | Named |
<slot :x="x" /> |
Static | ⭐⭐⭐⭐ | Scoped |
v-if="$slots.x" |
Conditional | ⭐⭐⭐⭐ | Detect existence |
useSlots() |
Runtime | ⭐⭐⭐ | Used in JS |
6. $slots and useSlots
(1) Accessing Slots with $slots
(2) useSlots (Composition API)
<script setup>
const { useSlots } = Vue
const slots = useSlots()
// Check whether a slot exists
if (slots.header) {
console.log('Header slot exists')
}
// Render dynamically
if (slots.default) {
console.log('Default slot exists')
}
</script>
(3) $slots vs useSlots
| Dimension | $slots |
useSlots() |
|---|---|---|
| API type | Used directly in templates | Used in JS (Composition API) |
| Returns | The slots object | The slots object (in refs form) |
| Where | Inside template |
Inside <script setup> |
▶ Example: 5 Uses of $slots
▶ Try it YourselfOutput:
A bordered wrapper with three stacked areas — a grey Header content strip on top, the default content Default slot: main content in the middle, and a grey Footer content strip at the bottom — followed by a small grey line Detected slots: header, default, footer.
Because all three slots were passed, the Default fallback text never renders;
if you delete the <template #header>, that strip and the header key both disappear.
┌──────────────────────────────┐
│ Header content │
│ Default slot: main content │
│ Footer content │
│ Detected slots: header, default, footer │
└──────────────────────────────┘
7. Complete Example: A Customizable Card Component
▶ Example: BaseCard with Default + Named Slots
▶ Try it YourselfOutput:
Three stacked cards built from the same component.
The first shows only a body with Simple content (only body);
the second adds a grey header strip reading Product Name;
the third has a header (Order #12345), a body (Total: $99.99) and a footer with a green View Details button.
Because each card passes different slots, no No content (default) fallback ever appears.
┌─────────────────────────────┐
│ Simple content (only body) │
└─────────────────────────────┘
┌─────────────────────────────┐
│ Product Name (header) │
│ Product description │
└─────────────────────────────┘
┌─────────────────────────────┐
│ Order #12345 (header) │
│ Total: $99.99 │
│ [View Details] (footer) │
└─────────────────────────────┘
❓ FAQ
#header is shorthand for v-slot:header. The shorthand is recommended because it is shorter.<slot /> is the default. If you really want to name it, use <slot name="default" /> and let the parent use <template #default> or pass content directly.<slot>.$slots (in templates) or useSlots() (in JS). The child exposes them via <slot>, and the parent fills them with <template #name>.<slot name="header"><h3>Default Header</h3></slot> is shown when the parent passes no header.📖 Summary
- Slots are Vue's content distribution API: the parent controls the content, the child controls the position
- 3 kinds of slots: default (
<slot />), named (name="x"), scoped (:x="x") - The parent passes named slots with
<template #name>or the#nameshorthand - Scoped slots let the child pass data to the parent (slot props)
<slot>fallback content is shown when the parent passes nothing$slots/useSlots()check whether a slot exists- Slots and props work together: props pass configuration, slots pass content
📝 Exercises
-
Basic (difficulty ⭐) Implement a simple Button component:
- BaseButton.vue: default slot + 3 variants (primary/success/danger)
- props:
variant(String) - Parent: test all 3 variants + custom slot content
-
Intermediate (difficulty ⭐⭐) Implement a BaseLayout component:
- Child: 3 named slots (header / default / footer)
- Parent: build a Dashboard page with BaseLayout
- Use
$slotsto detect whether each slot exists - Add fallback content
-
Challenge (difficulty ⭐⭐⭐) Build a complete DataTable component system:
- DataTable.vue: receives columns + data and renders the table
- Scoped slots:
#cell-{key}lets the parent customize a cell - Default slot: shows the raw value when the parent provides nothing
- Parent examples: a user table + an order table, with 2 different cell renderers
- Performance: cache rows with v-memo (skip re-rendering when data is unchanged)
- TypeScript strong typing