Slots let one component control layout or behaviour while the parent controls the markup: the basis of layouts, cards, tables and lists.
Props pass data; slots pass template content. A component marks where content goes with <slot />; whatever the parent writes between the component's tags is rendered there. Content inside <slot> itself is the fallback shown when the parent passes nothing.
Named slots allow several insertion points. The child writes <slot name="header" /> and the parent targets it with <template #header>...</template> (# is short for v-slot:). Content outside any <template #...> goes to the default slot.
Slot content is compiled in the parent's scope: it sees the parent's variables, not the child's. When the child needs to give data to the slot, use a scoped slot: the child binds props on the slot, <slot name="item" :item="row" :index="i" />, and the parent receives them, typically destructured: <template #item="{ item, index }">.
Scoped slots enable renderless or headless components: the child owns the logic (fetching, pagination, selection) and the parent decides exactly how each piece looks.
Inside the child, useSlots() (or $slots in the template) tells you which slots were provided, which is handy for omitting a wrapper: <header v-if="$slots.header">. With TypeScript, defineSlots<{ item(props: { item: Row }): any }>() types the slot props.
Example
<!-- FancyList.vue -->
<script setup>
const { items } = defineProps({ items: Array })
</script>
<template>
<section>
<header v-if="$slots.header"><slot name="header" /></header>
<ul>
<li v-for="(item, i) in items" :key="item.id">
<slot name="item" :item="item" :index="i">{{ item.name }}</slot>
</li>
</ul>
<p v-if="!items.length"><slot name="empty">No items</slot></p>
</section>
</template>
<!-- Parent.vue (template part) -->
<FancyList :items="users">
<template #header>Team ({{ users.length }})</template>
<template #item="{ item, index }">
<strong>{{ index + 1 }}.</strong> {{ item.name }} <em>{{ item.role }}</em>
</template>
</FancyList>Remember
<slot />renders the parent's content; inner content is the fallback- Named slots:
<slot name="x">in the child,<template #x>in the parent - Scoped slots pass child data up:
<template #item="{ item }">
Watch out for
- Expecting slot content to read the child's variables; it only sees the parent's scope unless passed as slot props.