Chapter 28: Vue Slots
Slots (also called content distribution or composability outlets)
Slots are Vue’s way of letting a parent component inject custom content (HTML, components, text…) into a child component at specific places — without the child having to know in advance what that content will be.
In simple words:
The child says: “Hey parent, I have some holes in my design — you can put whatever you want here.” The parent says: “Great, here’s some custom content — put it in those holes.”
This is what makes components truly reusable and composable — the same component can look completely different depending on what the parent decides to put inside.
Why Slots Are So Important (Real-World Motivation)
Without slots you would have to:
- Pass huge strings via props → ugly & unsafe (v-html)
- Use lots of boolean props or variant props → explodes into dozens of props
- Duplicate almost-identical components → maintenance nightmare
With slots:
- One component can serve many different use-cases
- Parent controls layout & content
- Child controls structure & styling
- Perfect separation of concerns
Types of Slots in Vue 3 (2026)
- Default slot (unnamed / main content)
- Named slots (multiple specific places)
- Scoped slots (child passes data back to parent — most powerful)
- Dynamic slot names (rare but useful)
Let’s go through them one by one with real examples.
1. Default Slot (Most Common)
Child: Card.vue (a simple wrapper)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
<!-- src/components/Card.vue --> <template> <div class="card"> <div class="card-header"> <h3><slot name="header">Default Title</slot></h3> </div> <!-- Default slot = everything not in a named slot --> <div class="card-body"> <slot /> </div> <div class="card-footer"> <slot name="footer">Default Footer</slot> </div> </div> </template> <script setup lang="ts"> // no props needed for this example </script> <style scoped> .card { border: 1px solid #e5e7eb; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.06); max-width: 500px; margin: 1rem auto; background: white; } .card-header { background: #f8fafc; padding: 1rem 1.5rem; border-bottom: 1px solid #e5e7eb; } .card-body { padding: 1.5rem; } .card-footer { background: #f8fafc; padding: 1rem 1.5rem; border-top: 1px solid #e5e7eb; text-align: right; } </style> |
Parent usage (different content every time)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
<template> <div> <!-- Only body content --> <Card> This is a very simple card with just body text. </Card> <!-- Header + body + footer --> <Card> <template #header> <span>🚨 Important Notice</span> </template> <p>Payment due in 3 days. Please settle ₹4,999.</p> <template #footer> <button class="pay-btn">Pay Now</button> </template> </Card> <!-- Completely custom look --> <Card> <template #header> <img src="https://example.com/logo.png" alt="Logo" style="height: 40px;" /> </template> <div style="text-align:center; padding: 2rem;"> <h2>Welcome to Premium</h2> <p>Unlock all features for ₹999/month</p> </div> <template #footer> <small>Terms apply • Cancel anytime</small> </template> </Card> </div> </template> <script setup lang="ts"> import Card from '@/components/Card.vue' </script> <style scoped> .pay-btn { background: #22c55e; color: white; border: none; padding: 0.6rem 1.5rem; border-radius: 6px; cursor: pointer; } </style> |
→ Same <Card> component — completely different appearance & purpose
2. Named Slots (Multiple Specific Places)
Already shown in the example above — use <template #name> or v-slot:name
Shorthand syntax (very common):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<Card> <template #header>…</template> <template #footer>…</template> </Card> <!-- or even shorter (Vue 3.1+) --> <Card> #header <h3>Custom Header</h3> #footer <small>Footer text</small> </Card> |
3. Scoped Slots (Child → Parent Data Passing – The Superpower)
This is where slots become really powerful.
The child can pass data back to the parent so the parent can decide how to render that slot.
Child: ProductList.vue
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<template> <ul class="product-list"> <li v-for="product in products" :key="product.id"> <!-- scoped slot: pass product to parent --> <slot name="item" :product="product" :is-new="product.isNew"> <!-- default fallback if parent doesn't provide slot --> <span>{{ product.name }} - ₹{{ product.price }}</span> </slot> </li> </ul> </template> <script setup lang="ts"> defineProps<{ products: { id: number; name: string; price: number; isNew?: boolean }[] }>() </script> |
Parent usage – customize rendering
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<ProductList :products="products"> <!-- Scoped slot: receive :product and :is-new --> <template #item="{ product, isNew }"> <div class="custom-item"> <strong>{{ product.name }}</strong> <span class="price">₹{{ product.price.toLocaleString() }}</span> <span v-if="isNew" class="new-badge">NEW!</span> </div> </template> </ProductList> |
→ Parent decides how each item looks — child only provides the data & loop
Quick Summary Table – Slots in Vue 3 (2026)
| Slot Type | Syntax in Parent | Syntax in Child | Use Case / Power Level |
|---|---|---|---|
| Default slot | <Card>Content here</Card> | <slot /> | Main content ★★☆☆☆ |
| Named slot | <template #header>…</template> | <slot name=”header” /> | Multiple sections ★★★☆☆ |
| Scoped slot | <template #item=”{ product }”>…</template> | <slot name=”item” :product=”product”> | Child → parent data ★★★★★ |
| Fallback content | — | Default content inside <slot> | Safety net |
| Dynamic slot name | <template #[slotName]>…</template> | <slot :name=”dynamicName” /> | Rare / advanced |
Pro Tips from Real 2026 Projects
- Prefer scoped slots over passing render functions or huge props
- Use default slot content as fallback → makes component more forgiving
- Name slots clearly: header, footer, item, actions, icon, badge…
- For UI libraries → almost every component should have at least one slot
- Scoped slots + TypeScript → excellent autocompletion
Your mini practice:
- Create Modal.vue with header, body (default), footer slots
- Use it twice: one with simple text, one with form inside body
- Add scoped slot for footer that receives isLoading prop
Any part still fuzzy? Want me to show:
- Scoped slots with TypeScript typing?
- Slots vs props vs provide/inject?
- Real UI library example (Card with image slot, actions slot)?
- Fallback content + conditional slots?
Just tell me — we’ll build the next beautiful example together 🚀
Happy slotting from Hyderabad! 💙
