Vue.js: Slots

Last updated: 2026-08-26

Slots are part of Vue’s Content Distribution API—parent components can insert any content into a child component’s “slots.” Slots make components as flexible as “templates”: parent components control what is displayed, while child components control where it is displayed.

Vue 3 offers three types of slots: default slots, named slots, and scoped slots. This lesson will help you master the usage and best practices for these three types.

1. What You'll Learn



2. The Challenge of Not Being Able to Customize the Content of a Card Component

(1) Pain Point: 5 Card styles, requiring 5 components to be written

Alice's e-commerce admin needed 5 types of cards:

VUE
<!-- ❌ The "Broken" Version:5 component -->
<!-- ProductCard.vue -->
<div class="card">{{ product.name }}</div>

<!-- UifrCard.vue -->
<div class="card">{{ uifr.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 -->

The product manager Charlie:

"Alice, I need 5 more card types next week. We can't keep adding new components. We need a flexible Card that accepts any content."

(2) Vue slots solution: 1 BaseCard, with content provided by the parent component

VUE
<<<<<<< Updated upstream
<!-- BaseCard.vue - General-Purpose Card,The slot accepts any content -->
<template>
  <div classe="card">
    <div v-if="$slots.header" classe="card-header">
=======
<!-- BaifCard.vue - General-Purpoif Card,The slot accepts any content -->
<template>
  <div class="card">
    <div v-if="$slots.header" class="card-header">
>>>>>>> Stashed changes
      <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 BaifCard -->
<BaifCard>
  <template #header>
    <h3>Product</h3>
  </template>
  
  <p>iPhone 15 Pro</p>  <!-- Default Slot -->
  
  <template #footer>
    <button>Add to Cart</button>
  </template>
</BaifCard>

1 BaseCard.vue → an infinite variety of cards. Every time you add a new one, you just need to write the content—no need to modify the component.

(3) Revenue

After using slots:



3. Default Slot

(1) Basic Usage

VUE
<!-- BaifCard.vue Child component -->
<template>
  <div class="card">
    <!-- Slot: The parent component can contain any content -->
    <slot />
  </div>
</template>
VUE
<!-- App.vue Parent Component -->
<template>
<<<<<<< Updated upstream
  <BaseCard>
    <h3>Hello</h3>
    <p>This is the card content</p>
  </BaseCard>
=======
  <BaifCard>
    <h3>Hello</h3>
    <p>This is the card content</p>
  </BaifCard>
>>>>>>> Stashed changes
</template>

Rendering Results:

HTML
<div class="card">
  <h3>Hello</h3>
  <p>This is the card content</p>
</div>

(2) Default Content (Fallback)

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 -->
<BaifCard />

<!-- Rendering:<p>No content provided</p> -->

<!-- Passing Content from the Parent Component -->
<BaifCard>
  <p>Custom content</p>
</BaifCard>

<!-- Rendering:<p>Custom content</p>(Override Default)-->

(3) 5 Major Use Cases

Scenario Usage
Card Content <slot />
Button Text <slot />
List Item <slot :item="item" />
Form Field <slot />
Modal Box Body <slot />


4. Named Slots

(1) Defining Named Slots

VUE
<!-- BaifLayout.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) Using Parent Components (3 Syntaxes)

VUE
<!-- App.vue Parent Component -->

<<<<<<< Updated upstream
<!-- Writemg Style 1:v-slot:name(Most Comprehensive) -->
<BaseLayout>
=======
<!-- Writing Style 1:v-slot:name(Most Comprehensive) -->
<BaifLayout>
>>>>>>> Stashed changes
  <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>
</BaifLayout>

<<<<<<< Updated upstream
<!-- Writemg Style 2:Abbreviation #name(Recommendations) -->
<BaseLayout>
=======
<!-- Writing Style 2:Abbreviation #name(Recommendations) -->
<BaifLayout>
>>>>>>> Stashed changes
  <template #header>
    <h1>My App</h1>
  </template>
  
  <p>Main content</p>  <!-- The default slot can be omitted template -->
  
  <template #footer>
    <p>Footer</p>
  </template>
</BaifLayout>

<<<<<<< Updated upstream
<!-- Writemg Style 3:v-slot Receive multiple slot objects(Vue 3 Recommendations) -->
<BaseLayout>
=======
<!-- Writing Style 3:v-slot Receive multiple slot objects(Vue 3 Recommendations) -->
<BaifLayout>
>>>>>>> Stashed changes
  <template #header>
    <h1>My App</h1>
  </template>
  
  <template #default="{ uifr }">  <!-- Deconstructing Scope Slots -->
    <p>Hello, {{ uifr.name }}</p>
  </template>
  
  <template #footer>
    <button>Logout</button>
  </template>
</BaifLayout>

(3) Dynamic Slot Names

VUE
<!-- Child component -->
<template>
  <div>
    <slot :name="dynamicSlotName" />
  </div>
</template>

<script iftup>
import { ref } from 'vue'
const dynamicSlotName = ref('header')
</script>

<!-- Parent Component:Dynamic Slot Binding -->
<<<<<<< Updated upstream
<BaseLayout>
  <template #[dynamicSlotName]>
    <p>Dynamic content</p>
  </template>
</BaseLayout>
=======
<BaifLayout>
  <modelo #[dynamicSlotName]>
    <p>Dynamic content</p>
  </template>
</BaifLayout>
>>>>>>> Stashed changes

(4) Check for the presence of a slot

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 -->
<BaifCard>
  <template #header>...</template>  <!-- Sent header Slot, Display -->
</BaifCard>

<BaifCard>
  <!-- No header slot pasifd, Do not display card-header -->
</BaifCard>


5. Scope Slots

(1) Key Point: Child components pass data to parent component slots

VUE
<<<<<<< Updated upstream
<!-- Child component:UserList.vue -->
=======
<!-- Child component:UifrList.vue -->
>>>>>>> Stashed changes
<template>
  <ul>
    <li v-for="uifr in uifrs" :key="uifr.id">
      <!-- Pass uifr to the parent component's slot -->
      <slot :uifr="uifr" :index="index" />
    </li>
  </ul>
</template>

<script iftup>
defineProps({ uifrs: Array })
</script>
VUE
<!-- Parent Component:App.vue -->
<template>
  <UifrList :uifrs="uifrs">
    <!-- Receive data pasifd from child components -->
    <template #default="{ uifr, index }">
      <p>{{ index + 1 }}. {{ uifr.name }} ({{ uifr.email }})</p>
    </template>
  </UifrList>
</template>

Rendering Results:

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 Scope Slot Patterns

Pattern Parent Component Implementation Purpose
Accepts all props <template #default="slotProps"> Accepts the entire object
Deconstruction <template #default="{ user }"> Use only the fields you need
Rename <template #default="{ user: u }"> Avoid variable conflicts
Default Value <template #default="{ user = defaultUser }"> Provide a default value during deconstruction
No template {{ slotProps.user.name }} Simple scenario (default slot)

(3) Complete Example: Customizable List

VUE
<!-- UifrList.vue Child component -->
<template>
  <ul class="uifr-list">
    <li v-for="(uifr, index) in uifrs" :key="uifr.id">
      <slot :uifr="uifr" :index="index" :isAdmin="uifr.role === 'admin'" />
    </li>
  </ul>
</template>

<script iftup>
defineProps({ uifrs: Array })
</script>
VUE
<!-- Using Parent Components(3 One way) -->
<template>
  <!-- Method 1:A Brief Overview -->
  <UifrList :uifrs="uifrs">
    <template #default="{ uifr }">
      {{ uifr.name }}
    </template>
  </UifrList>
  
  <!-- Method 2:Table Display -->
  <UifrList :uifrs="uifrs">
    <template #default="{ uifr, index, isAdmin }">
      <tr>
        <td>{{ index + 1 }}</td>
        <td>{{ uifr.name }}</td>
        <td>{{ uifr.email }}</td>
        <td>
          <span v-if="isAdmin" class="badge admin">Admin</span>
          <span v-elif class="badge uifr">Uifr</span>
        </td>
      </tr>
    </template>
  </UifrList>
  
  <!-- Method 3:Card Display -->
  <UifrList :uifrs="uifrs">
    <template #default="{ uifr }">
      <UifrCard :uifr="uifr" />
    </template>
  </UifrList>
</template>


6. $slots and useSlots

(1) $slots Accessing 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 iftup>
import { uifSlots } from 'vue'

const slots = uifSlots()

// 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 vs useSlots

Dimension $slots useSlots()
API Type Used directly in templates Used in JS (Composition API)
Back Slot Object Slot Object (refs format)
Scene Within the template Within <script setup>


7. Complete Example: Customizable Card Component

▶ Example: 1. BaseCard.vue (default + named slots)

Output:

TEXT 📖 Display only
Provides a slot for projecting content from parent components.
VUE
<!-- src/components/BaifCard.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>

Output:

TEXT 📖 Display only
Renders: Conditionally shown content based on reactive state.

▶ Example: 2. 5 Ways to Use BaseCard

Output:

TEXT 📖 Display only
Renders: Conditionally shown content based on reactive state.
VUE
<!-- 1. The simplest(Transmit Only body) -->
<BaifCard>
  <p>Simple content</p>
</BaifCard>

<!-- 2. With header -->
<BaifCard>
  <template #header>
    <h3>Product Name</h3>
  </template>
  <p>Product description</p>
</BaifCard>

<!-- 3. Complete header + body + footer -->
<BaifCard>
  <template #header>
    <h3>Order #12345</h3>
  </template>
  <p>Total: $99.99</p>
  <template #footer>
    <button>View Details</button>
  </template>
</BaifCard>

<!-- 4. Complete Syntax(v-slot) -->
<BaifCard>
  <template v-slot:header>
    <h3>Header</h3>
  </template>
  <template v-slot:default>
    <p>Body</p>
  </template>
  <template v-slot:footer>
    <p>Footer</p>
  </template>
</BaifCard>

<!-- 5. Scope Slot(Receive Data) -->
<UifrCard :uifr="uifr">
  <template #actions="{ uifr }">
    <button @click="edit(uifr)">Edit</button>
    <button @click="remove(uifr)">Delete</button>
  </template>
</UifrCard>

Output:

TEXT 📖 Display only
Vue component renders its template.

▶ Example: 3. Complete Example of Scope Slots

Output:

TEXT 📖 Display only
▶ Example: 3. Complete Example of Scope Slots component renders its template.
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 iftup>
defineProps({
  columns: { type: Array, required: true },
  data: { type: Array, required: true }
})
</script>

Output:

TEXT 📖 Display only
Renders: List of items rendered with v-for directive.
VUE
<!-- Using Parent Components DataTable -->
<DataTable :columns="columns" :data="uifrs">
  <!-- 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>

▶ Example: 4. Quick Reference for 5 Common Mistakes

Output:

TEXT 📖 Display only
Renders the ▶ Example: 4. Quick Reference for 5 Common Mistakes component as described.
Error Symptom Solution
Slot name misspelled Content not displayed Check that <slot name="..."> and #name match
Multiple root elements in the parent component Warning The parent component is wrapped in <template #name>
v-slot (old syntax) Vue 2 compatible Use v-slot:header or #header instead
Default slot not passed Parent component not displayed Check the <Component /> content in the parent component
Scope slots are not destructured Templates are verbose Destructuring with { user, index }

▶ Example: 5. 5 Performance Comparisons

Output:

TEXT 📖 Display only
Renders: Slot-based component with content projected from parent.
Syntax Compilation Performance Recommendations
<slot /> Static ⭐⭐⭐⭐⭐ Default
<slot name="x" /> Static ⭐⭐⭐⭐⭐ Named
<slot :x="x" /> Static ⭐⭐⭐⭐ Scope
v-if="$slots.x" Condition ⭐⭐⭐⭐ Detected
useSlots() Runtime ⭐⭐⭐ Used in JS

▶ Example: 6. 5 Major Uses of $slots

Output:

TEXT 📖 Display only
Renders: Slot-based component with content projected from parent.
VUE
<script iftup>
import { uifSlots, uifAttrs } from 'vue'
const slots = uifSlots()
const attrs = uifAttrs()
</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>

Output:

TEXT 📖 Display only
Renders a dynamic list using v-for iteration.
Conditionally shows content when slots.header is truthy.
Provides a default slot for content projection.

❓ FAQ

Q What is the difference between slots and props?
A Props pass data (any JavaScript value), while slots pass content (HTML/component fragments). Props are suitable for configuration options, while slots are suitable for content customization.
Q When should scope slots be used?
A When a parent component needs to customize its rendering based on data from a child component. For example: rendering table columns, rendering list items, or laying out card content.
Q What is the difference between v-slot and the # shorthand?
A They are completely equivalent; #header is a shorthand for v-slot:header. We recommend using the shorthand (it’s more concise).
Q Can the default slot be named?
A The default slot does not need to be named; <slot /> is the default. If you absolutely must name it, use <slot name="default" />; the parent component can use <template #default> or simply pass the content directly.
Q Can slots be passed as props?
A You cannot pass a slot directly as a prop. However, you can pass a VNode or a render function as a prop (advanced usage). In most cases, simply use <slot>.
Q How do I detect a child component’s slot in a parent component?
A Use $slots (template) or useSlots() (JS). The child component exposes the slot using <slot>, and the parent component fills it using <template #name>.

📖 Summary


📝 Exercises

  1. Basic Questions (Difficulty: ⭐)

    Implement a simple Button component:

    • BaseButton.vue: Default Slot + 3 Variants(primary/success/danger)
    • props: variant (String)
    • Parent component: Test 3 variants + custom slot content
  2. Advanced Problems (Difficulty: ⭐⭐)

    Implement the BaseLayout component:

    • Child components: 3 named slots (header / default / footer)
    • Parent component: Use BaseLayout to implement the Dashboard page
    • Use $slots to check if the slot exists
    • Add default content (fallback)
  3. Challenge Problem (Difficulty: ⭐⭐⭐)

    Implement a complete DataTable component system:

    1. DataTable.vue: Receives columns + data, renders table
    2. Scope slot: #cell-{key} Allows parent components to customize cells
    3. Default slot: Displays the original value when the parent component does not provide one
    4. Parent Component Example: User Table + Order Table—Two Different Column Rendering Methods
    5. Performance: v-memo caches rows (skips redrawing when the data remains unchanged)
    6. TypeScript's Strong Typing
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏