Vue Scoped Slots

Pass data from child to parent through slot content

🔄 What are Scoped Slots?

Scoped slots let child components pass data back to the parent through slot content. This powerful pattern enables the parent to control rendering while accessing child component data, creating flexible and reusable components.


<!-- Child passes data -->
<slot :user="user" :index="i"></slot>

<!-- Parent receives data -->
<template #default="{ user, index }">
  <p>{{ user.name }}</p>
</template>
                                    

Scoped Slot Benefits

📤

Data Sharing

Child shares data with parent

<slot :item="item">
</slot>
🎨

Custom Rendering

Parent controls how data displays

<template #default="{ item }">
  <div>{{ item }}</div>
</template>
🔄

Reusability

Same logic, different presentations

<List>
  <template #default="{ item }">
    <!-- Custom view -->
  </template>
</List>
âš¡

Flexibility

Powerful component patterns

<template #item="props">
  {{ props.data }}
</template>

🔹 Basic Scoped Slot

Create a list component that passes item data to parent:

🔸 Child Component (ItemList.vue)

<template>
  <div class="list">
    <div v-for="(item, index) in items" :key="index">
      <slot :item="item" :index="index"></slot>
    </div>
  </div>
</template>

<script setup>
defineProps({
  items: Array
})
</script>

🔸 Parent Component

<template>
  <ItemList :items="fruits">
    <template #default="{ item, index }">
      <p>{{ index + 1 }}. {{ item }}</p>
    </template>
  </ItemList>
</template>

<script setup>
import { ref } from 'vue'
const fruits = ref(['Apple', 'Banana', 'Orange'])
</script>

Output:

1. Apple

2. Banana

3. Orange

🔹 User List with Scoped Slots

Build a flexible user list component:

<!-- Child Component (UserList.vue) -->
<template>
  <div class="user-list">
    <div v-for="user in users" :key="user.id" class="user-item">
      <slot :user="user" :isOnline="user.online">
        <!-- Default fallback -->
        <p>{{ user.name }}</p>
      </slot>
    </div>
  </div>
</template>

<!-- Parent Usage -->
<template>
  <UserList :users="userList">
    <template #default="{ user, isOnline }">
      <div>
        <h3>{{ user.name }}</h3>
        <span :style="{ color: isOnline ? 'green' : 'gray' }">
          {{ isOnline ? 'Online' : 'Offline' }}
        </span>
      </div>
    </template>
  </UserList>
</template>

🔹 Table with Scoped Slots

Create a reusable table with custom cell rendering:

<!-- Child Component (DataTable.vue) -->
<template>
  <table>
    <thead>
      <tr>
        <th v-for="col in columns" :key="col">{{ col }}</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(row, index) in data" :key="index">
        <td>
          <slot :row="row" :index="index"></slot>
        </td>
      </tr>
    </tbody>
  </table>
</template>

<!-- Parent Usage -->
<template>
  <DataTable :data="products" :columns="['Name', 'Price']">
    <template #default="{ row }">
      <td>{{ row.name }}</td>
      <td>${{ row.price.toFixed(2) }}</td>
    </template>
  </DataTable>
</template>

🔹 Passing Methods Through Scoped Slots

Share functions from child to parent:

<!-- Child Component (TodoList.vue) -->
<template>
  <div>
    <div v-for="todo in todos" :key="todo.id">
      <slot 
        :todo="todo" 
        :toggle="() => toggleTodo(todo.id)"
        :remove="() => removeTodo(todo.id)"
      ></slot>
    </div>
  </div>
</template>

<script setup>
const toggleTodo = (id) => { /* logic */ }
const removeTodo = (id) => { /* logic */ }
</script>

<!-- Parent Usage -->
<template>
  <TodoList>
    <template #default="{ todo, toggle, remove }">
      <div>
        <input type="checkbox" @change="toggle" />
        <span>{{ todo.text }}</span>
        <button @click="remove">Delete</button>
      </div>
    </template>
  </TodoList>
</template>

🔹 Multiple Scoped Slots

Use named scoped slots for different sections:

<!-- Child Component (Card.vue) -->
<template>
  <div class="card">
    <header>
      <slot name="header" :title="title" :date="date"></slot>
    </header>
    <main>
      <slot name="content" :body="body"></slot>
    </main>
    <footer>
      <slot name="footer" :author="author"></slot>
    </footer>
  </div>
</template>

<!-- Parent Usage -->
<template>
  <Card>
    <template #header="{ title, date }">
      <h2>{{ title }}</h2>
      <small>{{ date }}</small>
    </template>
    
    <template #content="{ body }">
      <p>{{ body }}</p>
    </template>
    
    <template #footer="{ author }">
      <span>By {{ author }}</span>
    </template>
  </Card>
</template>

🔹 Destructuring with Default Values

Provide fallback values when destructuring:

<template>
  <ProductList>
    <template #default="{ product, discount = 0, inStock = true }">
      <div>
        <h3>{{ product.name }}</h3>
        <p>Price: ${{ product.price * (1 - discount) }}</p>
        <span v-if="inStock">Available</span>
        <span v-else>Out of Stock</span>
      </div>
    </template>
  </ProductList>
</template>

Tip: Use default values in destructuring to handle optional slot props gracefully.

🧠 Test Your Knowledge

What do scoped slots allow you to do?