How to use scoped slots in Vue
Scoped slots in Vue enable child components to pass data back to parent slot content, creating powerful template patterns where child components provide data for parent-defined rendering logic. As the creator of CoreUI, a widely used open-source UI library, I’ve implemented scoped slots in countless Vue components for data tables, lists, and flexible UI patterns where parent components need access to child data. From my expertise, the most effective approach is using slot props to pass data from child to parent with template v-slot directive for clean consumption. This method provides powerful component composition with data flow from child to parent and highly customizable rendering patterns.
Use slot props to pass data from child components to parent slot content with template v-slot directive.
<!-- Child Component: UserList.vue -->
<template>
<div class="user-list">
<h3>Users</h3>
<div
v-for="user in users"
:key="user.id"
class="user-item"
>
<!-- Scoped slot passing user data to parent -->
<slot
name="user"
:user="user"
:isActive="user.status === 'active'"
:actions="{ edit: editUser, delete: deleteUser }"
>
<!-- Fallback content -->
<div>{{ user.name }} - {{ user.email }}</div>
</slot>
</div>
<!-- Default slot with summary data -->
<slot
name="summary"
:totalUsers="users.length"
:activeUsers="activeUsersCount"
>
<p>Total: {{ users.length }} users</p>
</slot>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const users = ref([
{ id: 1, name: 'John Doe', email: '[email protected]', status: 'active' },
{ id: 2, name: 'Jane Smith', email: '[email protected]', status: 'inactive' }
])
const activeUsersCount = computed(() =>
users.value.filter(user => user.status === 'active').length
)
const editUser = (user) => {
console.log('Editing user:', user.name)
}
const deleteUser = (user) => {
users.value = users.value.filter(u => u.id !== user.id)
}
</script>
<!-- Parent Component -->
<template>
<div>
<UserList>
<!-- Using scoped slot with custom rendering -->
<template #user="{ user, isActive, actions }">
<div class="custom-user-card" :class="{ active: isActive }">
<img :src="`/avatars/${user.id}.jpg`" :alt="user.name">
<div class="user-info">
<h4>{{ user.name }}</h4>
<p>{{ user.email }}</p>
<span v-if="isActive" class="status-badge">Active</span>
</div>
<div class="user-actions">
<button @click="actions.edit(user)">Edit</button>
<button @click="actions.delete(user)">Delete</button>
</div>
</div>
</template>
<!-- Using scoped slot for summary -->
<template #summary="{ totalUsers, activeUsers }">
<div class="summary-card">
<h4>User Statistics</h4>
<p>Total Users: {{ totalUsers }}</p>
<p>Active Users: {{ activeUsers }}</p>
<p>Inactive Users: {{ totalUsers - activeUsers }}</p>
</div>
</template>
</UserList>
</div>
</template>
Scoped slots pass data from child to parent using slot props in the slot definition. Parent components access this data using template v-slot with destructuring syntax. This enables child components to provide data while parent components control the rendering logic, creating powerful and flexible component patterns.
Best Practice Note:
This is the same scoped slot pattern we use in CoreUI Vue components for data tables, card lists, and customizable UI elements where flexible rendering is essential.