Vue.js course Β· Module 8: Slots & Dynamic Components
Scoped Slots
In this lesson5
In NOVA LAB, the inventory system provides data about every cargo module, but it is the operator who decides how to display it - as a list, a table or cards. Scoped slots are the mechanism for passing data from the child to the slot content written by the parent.
The Problem
Regular slots give the parent control over the look, but what if the child has data the parent needs? For example, the child iterates over a list of modules and wants to hand the parent control over how each item is rendered. From the previous lessons you know that slot content is compiled in the parent's scope, so the item variable from the v-for loop inside the child is invisible to it. You need a data cable running through the docking port in the opposite direction.
Passing Data Through a Slot
The child passes data as attributes on the <slot> tag, exactly the way you pass props to components. These values are called slot props. The ModuleList below renders the slot on every loop iteration with the current item and its number:
1<!-- ModuleList.vue (child) -->
2<script setup>
3import { ref } from 'vue'
4const items = ref([
5 { id: 1, name: 'Oxygen Tank', status: 'loaded' },
6 { id: 2, name: 'Water Filter', status: 'pending' },
7 { id: 3, name: 'Solar Cell', status: 'damaged' }
8])
9</script>
10
11<template>
12 <div class="module-list">
13 <div v-for="(item, index) in items" :key="item.id">
14 <slot :item="item" :index="index">
15 <!-- Fallback: simple rendering -->
16 <p>{{ item.name }}</p>
17 </slot>
18 </div>
19 </div>
20</template>The slot is rendered three times, each time with a different module's data. The fallback still works as in the first lesson: a parent that provides no content gets a simple list of names.
Receiving Data in the Parent
The parent receives the slot props with the v-slot directive and a variable name. Vue passes all slot props in one object, here called slotProps, and because the value of v-slot behaves like a function parameter, you can destructure it right away:
1<template>
2 <!-- Full syntax -->
3 <ModuleList v-slot="slotProps">
4 <div class="card">
5 <h3>{{ slotProps.item.name }}</h3>
6 <span>{{ slotProps.item.status }}</span>
7 </div>
8 </ModuleList>
9
10 <!-- With destructuring -->
11 <ModuleList v-slot="{ item, index }">
12 <div class="card">
13 <h3>#{{ index + 1 }}: {{ item.name }}</h3>
14 <span :class="item.status">{{ item.status }}</span>
15 </div>
16 </ModuleList>
17</template>Both notations are correct and give the same data; destructuring simply saves you from repeating slotProps.. We add 1 to index because loop numbering starts at zero. The data did not change owner: the list still belongs to ModuleList, and the parent only gets a view of each item for the time of rendering.
An Explicit Default Slot
Putting v-slot on the component tag is a shorthand for the case when you use only the default slot. You can describe the same slot explicitly with a <template> tag and #default, and write the destructuring into its value:
1<template>
2 <ModuleList>
3 <template #default="{ item }">
4 <p>{{ item.name }}</p>
5 </template>
6 </ModuleList>
7</template>The effect is identical to the previous one. The <template> version becomes mandatory, though, when you fill named slots next to the default slot - Vue then doesn't allow v-slot on the component itself and reports a compilation error, so there is no doubt which slot the data belongs to.
Named Scoped Slots
Scoped slots also work with named slots. The DataTable below has two ports: header for the table header and row, which receives item in every row. We assume the component keeps the items list in its script, just like ModuleList above. The parent receives the data with #row="{ item }":
1<!-- DataTable.vue -->
2<template>
3 <table>
4 <thead>
5 <tr>
6 <slot name="header">
7 <th>Name</th>
8 <th>Status</th>
9 </slot>
10 </tr>
11 </thead>
12 <tbody>
13 <tr v-for="item in items" :key="item.id">
14 <slot name="row" :item="item">
15 <td>{{ item.name }}</td>
16 <td>{{ item.status }}</td>
17 </slot>
18 </tr>
19 </tbody>
20 </table>
21</template>
22
23<!-- Parent -->
24<DataTable>
25 <template #header>
26 <th>Module Name</th>
27 <th>Current Status</th>
28 <th>Actions</th>
29 </template>
30
31 <template #row="{ item }">
32 <td>{{ item.name }}</td>
33 <td>{{ item.status }}</td>
34 <td><button>Inspect</button></td>
35 </template>
36</DataTable>The parent added a third column with an "Inspect" button, and DataTable did not change a single line. Note that the name attribute is not included in the slot props - Vue reserves it, so item is the only value the parent receives.
My advice: name slot props the way you would name component props - item, index, isSelected - and destructure them right in v-slot. The parent's code then reads like a regular template. Scoped slots will come back in the lesson on advanced patterns, where we build a component that renders no HTML of its own at all. In the editor below, ModuleList appears twice: with the default fallback and with cards that the parent builds from item and index.
Remember: a scoped slot is a port with a data cable - the child sends information through it, and the parent decides how to show it.
Code for this lesson: App.vue
1<script setup>
2import ModuleList from './ModuleList.vue'
3</script>
4
5<template>
6 <div class="nova-lab">
7 <h1>Scoped Slots Demo</h1>
8
9 <h2>Default rendering (fallback):</h2>
10 <ModuleList />
11
12 <h2>Custom card rendering:</h2>
13 <ModuleList v-slot="{ item, index }">
14 <div class="custom-card">
15 <span class="index">#{{ index + 1 }}</span>
16 <strong>{{ item.name }}</strong>
17 <span
18 class="status"
19 :class="item.status"
20 >
21 {{ item.status }}
22 </span>
23 <span class="weight">{{ item.weight }} kg</span>
24 </div>
25 </ModuleList>
26 </div>
27</template>
28
29<style scoped>
30.nova-lab {
31 background: #0a0e27;
32 color: #00ff88;
33 min-height: 100vh;
34 padding: 20px;
35 font-family: monospace;
36}
37h1 { color: #00ff88; margin-bottom: 20px; }
38h2 { color: #00b4d8; margin: 20px 0 10px; font-size: 14px; }
39.custom-card {
40 display: flex;
41 align-items: center;
42 gap: 12px;
43 padding: 10px 15px;
44 border: 1px solid #00b4d8;
45 margin: 5px 0;
46 background: rgba(0, 180, 216, 0.05);
47}
48.index {
49 color: #666;
50 font-size: 12px;
51}
52.status {
53 padding: 2px 8px;
54 font-size: 11px;
55 border-radius: 3px;
56}
57.status.loaded { background: #00ff88; color: #0a0e27; }
58.status.pending { background: #ff9900; color: #0a0e27; }
59.status.damaged { background: #ff4444; color: #fff; }
60.weight { color: #666; margin-left: auto; }
61</style>Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What are scoped slots used for in Vue?
2. How does a child component pass data to a scoped slot?
These are 2 of 3 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Create a ModuleList component that iterates over a list and passes each item through a scoped slot. The parent renders the item in its own way.
- Click in order
Arrange the code for receiving data from a scoped slot in the parent:
- Code editor
Create a DataTable component with named scoped slots: #header for the header and #row="{ item }" for the table row.