Chapter 29: Vue v-slot
v-slot (and its shorthand #)
This is the official, modern way to work with slots in Vue 3 (since Vue 2.6+ actually, but fully mature in Vue 3).
v-slot replaced the old confusing slot=”name” and slot-scope syntax. It is cleaner, more readable, and supports scoped slots beautifully with TypeScript.
Quick Mental Model – What is v-slot?
Think of a component as a template with holes.
The child component says:
“I have some named holes here — header, footer, actions, item… You (the parent) can decide what content goes into each hole.”
The parent uses v-slot (or #) to fill those holes with custom content.
Syntax Evolution (Why We Use v-slot Now)
Old Vue 2 style (don’t use this anymore)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<!-- old named slot --> <slot name="header"></slot> <!-- old scoped slot --> <slot name="item" :product="product"></slot> <!-- parent --> <template slot="header">…</template> <template slot-scope="{ product }">…</template> |
Modern Vue 3 style (2026 standard)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<!-- child --> <slot name="header" /> <slot name="item" :product="product" :is-new="product.isNew" /> <!-- parent --> <template #header>…</template> <template #item="{ product, isNew }">…</template> <!-- or even shorter shorthand (very common) --> <Card> #header <h3>Custom Header</h3> #item="{ product }" <div>{{ product.name }}</div> </Card> |
Real, Practical Example – Modal with Named & Scoped Slots
Child Component: Modal.vue (reusable modal)
|
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 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
<!-- src/components/Modal.vue --> <template> <div class="modal-overlay" @click.self="emitClose"> <div class="modal-content"> <!-- Named slot: header --> <div class="modal-header"> <slot name="header"> <!-- default fallback content --> <h2>Default Modal Title</h2> </slot> <button class="close-btn" @click="emitClose">×</button> </div> <!-- Default slot: main body content --> <div class="modal-body"> <slot /> </div> <!-- Named slot: footer --> <div class="modal-footer"> <slot name="footer"> <!-- default buttons --> <button @click="emitClose">Close</button> </slot> </div> </div> </div> </template> <script setup lang="ts"> const emit = defineEmits<{ (e: 'close'): void }>() function emitClose() { emit('close') } </script> <style scoped> .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; } .modal-content { background: white; border-radius: 12px; width: 90%; max-width: 500px; max-height: 90vh; overflow-y: auto; box-shadow: 0 10px 30px rgba(0,0,0,0.25); } .modal-header { padding: 1.2rem 1.5rem; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center; } .close-btn { background: none; border: none; font-size: 1.8rem; cursor: pointer; color: #6b7280; } .modal-body { padding: 1.5rem; } .modal-footer { padding: 1rem 1.5rem; border-top: 1px solid #e5e7eb; text-align: right; } </style> |
Parent usage – different modals with same component
|
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 |
<template> <div> <button @click="showInfo = true">Show Info Modal</button> <button @click="showForm = true">Show Form Modal</button> <!-- Modal 1: simple info --> <Modal v-if="showInfo" @close="showInfo = false"> <template #header> <h2>Important Information</h2> </template> <p>This is some very important text that explains something critical.</p> <template #footer> <button @click="showInfo = false">Got it!</button> </template> </Modal> <!-- Modal 2: form with scoped slot example --> <Modal v-if="showForm" @close="showForm = false"> <template #header> <h2>Update Profile</h2> </template> <form @submit.prevent="handleSubmit"> <input v-model="form.name" placeholder="Name" /> <input v-model="form.email" placeholder="Email" /> </form> <template #footer> <button @click="showForm = false">Cancel</button> <button type="submit" @click="handleSubmit">Save</button> </template> </Modal> </div> </template> <script setup lang="ts"> import { ref } from 'vue' import Modal from '@/components/Modal.vue' const showInfo = ref(false) const showForm = ref(false) const form = ref({ name: '', email: '' }) function handleSubmit() { console.log('Form submitted:', form.value) showForm.value = false } </script> |
Scoped Slots – The Real Power (Child → Parent Data)
Child: ProductGrid.vue (passes product data to parent)
|
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 |
<template> <div class="grid"> <div v-for="product in products" :key="product.id" class="grid-item"> <!-- scoped slot: send product & helpers back to parent --> <slot name="product" :product="product" :is-new="product.createdAt > Date.now() - 7*24*60*60*1000" :formatPrice="formatPrice"> <!-- fallback if parent doesn't use slot --> <div>{{ product.name }} - {{ formatPrice(product.price) }}</div> </slot> </div> </div> </template> <script setup lang="ts"> defineProps<{ products: { id: number; name: string; price: number; createdAt: number }[] }>() function formatPrice(value: number) { return `₹{value.toLocaleString()}` } </script> |
Parent – custom rendering using scoped slot
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<ProductGrid :products="products"> <template #product="{ product, isNew, formatPrice }"> <div class="custom-product"> <h4>{{ product.name }}</h4> <p class="price">{{ formatPrice(product.price) }}</p> <span v-if="isNew" class="new-tag">NEW!</span> </div> </template> </ProductGrid> |
Quick Syntax Reference – v-slot /
| Slot Type | Child (defines) | Parent (fills) – modern | Parent – old / alternative |
|---|---|---|---|
| Default slot | <slot /> | <MyComp>content</MyComp> | — |
| Named slot | <slot name=”header” /> | <template #header>…</template> | <template v-slot:header>…</template> |
| Scoped slot | <slot :item=”item” /> | <template #item=”{ item }”>…</template> | <template v-slot:item=”slotProps”>…</template> |
| Shorthand | — | #header or #item=”{ item }” | — |
Pro Tips from Real 2026 Projects
- Prefer shorthand # — much cleaner & most developers use it
- Always provide fallback content inside <slot> — makes component more robust
- Use scoped slots when child has data parent might want to customize rendering with
- Name slots clearly: header, footer, actions, item, empty, loading…
- Scoped slots + TypeScript → excellent DX (autocompletion for slot props)
Your mini homework:
- Create Accordion.vue with default slot for content and named slot for title
- Use it 3 times with different content
- Add a scoped slot for icon that receives isOpen state
Any part confusing? Want me to show:
- Scoped slots with TypeScript interface?
- Dynamic slot names?
- Slots vs props vs provide/inject comparison?
- Real UI library pattern (Card, Modal, Table with slots)?
Just tell me — we’ll build the next beautiful slotted component together 🚀
Happy slotting from Hyderabad! 💙
