Chapter 30: Vue Scoped Slots
Scoped Slots
This is the feature that truly makes Vue components composable powerhouses. Once you master scoped slots, you will feel like you unlocked a cheat code for building flexible, reusable UI libraries and complex layouts.
First — Very Clear Mental Model
A normal (non-scoped) slot is like this:
Child component: “I have a hole here — parent, give me any HTML/content you want.” Parent: “Here’s some markup — put it in the hole.”
A scoped slot adds one super-important twist:
Child component: “I have a hole here — and by the way, here is some data I have (product, item, user, isOpen, index…). Parent, you can use that data to decide how to fill the hole.”
In other words:
- Child provides data to the parent
- Parent decides the rendering using that data
This reverses the usual data-flow direction for that particular piece of content — child → parent.
Classic Real-World Example Everyone Should Build Once
Child: ProductList.vue (a reusable list that doesn’t decide how items look)
|
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 |
<!-- src/components/ProductList.vue --> <template> <div class="product-grid"> <div v-for="(product, index) in products" :key="product.id" class="grid-item" > <!-- Scoped slot: pass lots of useful data back to parent --> <slot name="item" :product="product" :index="index" :is-new="product.createdAt > Date.now() - 7*24*60*60*1000" :is-even="index % 2 === 0" :format-price="formatPrice" > <!-- fallback content if parent doesn't provide the slot --> <div class="default-item"> {{ product.name }} — ₹{{ formatPrice(product.price) }} <span v-if="is-new" class="new-badge">NEW</span> </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('en-IN') } </script> <style scoped> .product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.5rem; } .grid-item { /* minimal base style */ } .default-item { padding: 1rem; background: #f8fafc; border-radius: 8px; } .new-badge { background: #fbbf24; color: #92400e; padding: 0.2rem 0.6rem; border-radius: 9999px; font-size: 0.8rem; margin-left: 0.5rem; } </style> |
Parent — total freedom how each item looks
|
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 85 86 87 88 89 90 91 92 93 94 95 96 |
<template> <div class="page"> <h1>Our Products</h1> <ProductList :products="products"> <!-- Scoped slot using shorthand #item --> <template #item="{ product, index, isNew, isEven, formatPrice }"> <div class="custom-product-card" :class="{ 'even-row': isEven }" > <div class="image-placeholder"></div> <h3 class="product-title"> {{ product.name }} <span v-if="isNew" class="new-tag">New Arrival!</span> </h3> <p class="price"> {{ formatPrice(product.price) }} </p> <p class="meta"> Position #{{ index + 1 }} in list </p> </div> </template> </ProductList> </div> </template> <script setup lang="ts"> import ProductList from '@/components/ProductList.vue' const products = [ { id: 1, name: 'Vue.js Mastery Course', price: 4999, createdAt: Date.now() - 3*24*60*60*1000 }, { id: 2, name: 'TypeScript Pro', price: 2999, createdAt: Date.now() - 10*24*60*60*1000 }, { id: 3, name: 'Tailwind + Vue Bundle', price: 7999, createdAt: Date.now() - 1*24*60*60*1000 } ] </script> <style scoped> .custom-product-card { padding: 1.5rem; background: white; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.08); transition: transform 0.2s; } .custom-product-card:hover { transform: translateY(-4px); } .even-row { background: #f8fafc; } .image-placeholder { height: 160px; background: linear-gradient(135deg, #667eea, #764ba2); border-radius: 8px; margin-bottom: 1rem; } .product-title { margin: 0 0 0.8rem; font-size: 1.3rem; } .price { font-size: 1.5rem; font-weight: bold; color: #15803d; margin: 0 0 0.5rem; } .new-tag { background: #fbbf24; color: #92400e; padding: 0.2rem 0.6rem; border-radius: 9999px; font-size: 0.8rem; margin-left: 0.6rem; } .meta { color: #6b7280; font-size: 0.9rem; } </style> |
Key Takeaways – Why Scoped Slots Are So Powerful
| Aspect | What it means | Why it matters in 2026 |
|---|---|---|
| Child controls data | Decides what data is available (:product, :index, :is-new, helper functions) | Child owns business logic |
| Parent controls rendering | Decides layout, classes, extra markup, icons, badges… | Parent owns UI decisions |
| Fallback content | <slot> can have default markup if parent doesn’t provide slot | Component is usable out-of-the-box |
| Multiple scoped slots | You can have several: #item, #empty, #loading, #header… | Very flexible layouts |
| TypeScript + scoped slots | defineSlots<{ item: { product: Product; isNew: boolean } }>() | Autocompletion & safety |
Common Scoped Slot Patterns You Will See Everywhere
- Lists / Tables / Grids → #item, #header, #empty, #loading
- Modals / Dialogs → #trigger (button that opens), #content
- Accordions / Tabs → #title, #content with :isOpen
- Dropdowns / Selects → #option with :item, :selected
- Data-driven cards → #card with :data, :index
Quick Syntax Reference (2026 Style)
| Goal | Child (provides slot) | Parent (uses slot) – recommended shorthand | Parent – full verbose syntax |
|---|---|---|---|
| Default scoped slot | <slot :item=”item” /> | <MyComp v-slot=”{ item }”>…</MyComp> | <template v-slot:default=”{ item }”>…</template> |
| Named scoped slot | <slot name=”item” :product=”product” /> | <template #item=”{ product }”>…</template> | <template v-slot:item=”{ product }”>…</template> |
| Shorthand without template tag | — | <MyComp #item=”{ product }”>…</MyComp> | — |
| Multiple slots | <slot name=”header” /> <slot name=”footer” /> | <template #header>…</template> #footer … | — |
Your Mini Practice Challenge
- Create Timeline.vue component that loops over events
- Use scoped slot#event that receives :event, :index, :is-last
- In parent → render each event differently (left/right alignment, icons…)
- Add fallback content inside <slot> so it works even without custom slot
Any part still confusing? Want to see:
- Scoped slots with TypeScript defineSlots typing?
- Scoped slot inside v-for vs outside?
- Real UI library pattern (DataTable, Modal, Accordion with slots)?
- Difference between scoped slots vs props vs provide/inject?
Just tell me — we’ll build another beautiful example together 🚀
Happy scoped-slotting from Hyderabad! 💙
