Child Hands Data to the Parent's Markup
Scoped Slots
A child can expose data to slot content through slot bindings. This is the trick behind renderless components and headless UI libraries.
What you'll learn
- Bind data on a slot
- Destructure in the parent with v-slot
- Build a renderless component
A scoped slot lets the child component pass values back to the parent’s slot content. The parent receives those values as props on the slot.
Binding Data on a Slot
<!-- UserList.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const users = ref([{ id: 1, name: 'Ada' }, { id: 2, name: 'Linus' }])
const loading = ref(false)
</script>
<template>
<div v-for="u in users" :key="u.id">
<slot :user="u" :loading="loading" />
</div>
</template> Consuming in the Parent
Use v-slot (or the # shorthand) to capture the slot props. Destructuring keeps it clean.
<UserList v-slot="{ user, loading }">
<p v-if="loading">Loading…</p>
<p v-else>{{ user.name }}</p>
</UserList> For named scoped slots, combine the name with the destructure:
<DataTable :rows="rows">
<template #cell="{ row, column }">
<strong>{{ row[column.key] }}</strong>
</template>
</DataTable> Renderless Components
A renderless component does logic only and exposes everything through scoped slots. The parent decides the markup.
<!-- Toggle.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const on = ref(false)
const toggle = () => (on.value = !on.value)
</script>
<template>
<slot :on="on" :toggle="toggle" />
</template> <Toggle v-slot="{ on, toggle }">
<button @click="toggle">{{ on ? 'Hide' : 'Show' }}</button>
<p v-if="on">Surprise!</p>
</Toggle>