Props pass data into components; slots pass template content. They are what make wrapper components -- cards, modals, layouts, buttons -- truly reusable, because the parent decides what appears inside while the child controls structure and style. This lesson covers default slots, fallback content, named slots, and scoped slots.
A <slot> outlet inside a child marks where parent-provided content should render:
<!-- BaseCard.vue -->
<template>
<div class="card">
<slot></slot>
</div>
</template>
<style scoped>
.card { border: 1px solid #ddd; border-radius: 8px; padding: 1rem; }
</style><!-- Parent -->
<BaseCard>
<h3>Any markup works here</h3>
<p>Including <em>other components</em>.</p>
</BaseCard>Slot content is compiled in the parent's scope: it can read the parent's state ({{ user.name }}) but not the child's internals. Remember it as: everything in the parent template sees parent data.
Content between the <slot> tags renders only when the parent provides nothing:
<!-- SubmitButton.vue -->
<template>
<button type="submit"><slot>Submit</slot></button>
</template><SubmitButton /> shows "Submit"; <SubmitButton>Save draft</SubmitButton> shows the custom label.
Layout-style components need several insertion points. Give each <slot> a name, and fill them with <template #name>:
<!-- PageLayout.vue -->
<template>
<header><slot name="header"></slot></header>
<main><slot></slot></main>
<footer><slot name="footer"></slot></footer>
</template><PageLayout>
<template #header>
<h1>Dashboard</h1>
</template>
<p>Main content goes to the default slot automatically.</p>
<template #footer>
<small>© 2026 CodeYourCraft</small>
</template>
</PageLayout>#header is shorthand for v-slot:header. Unwrapped content flows into the default slot.
Sometimes the child owns data the parent needs while writing slot content -- think of a list component that manages fetching, but lets callers decide how each row looks. The child binds props onto the slot outlet; the parent receives them:
<!-- UserList.vue -->
<script setup>
import { ref } from 'vue'
const users = ref([
{ id: 1, name: 'Ada', role: 'admin' },
{ id: 2, name: 'Linus', role: 'editor' }
])
</script>
<template>
<ul>
<li v-for="u in users" :key="u.id">
<slot :user="u"></slot>
</li>
</ul>
</template><!-- Parent decides row appearance -->
<UserList v-slot="{ user }">
<strong>{{ user.name }}</strong> — {{ user.role }}
</UserList>This inversion -- child provides data, parent provides rendering -- is one of Vue's most powerful reuse patterns. Component libraries use it everywhere: tables with custom cells, autocomplete with custom option templates, virtual scrollers.
Named slots can be scoped too: <slot name="row" :item="item"> is consumed with <template #row="{ item }">.
Occasionally a wrapper should skip markup when a slot is empty. $slots exposes provided slots in the template:
<footer v-if="$slots.footer" class="card-footer">
<slot name="footer"></slot>
</footer><slot> renders parent-supplied template content; fallback content sits between the tags.<slot name="x"> + <template #x>) enable multi-region layouts.$slots to detect whether optional slots were filled.Next up: The Composition API: ref, reactive, setup -- a deeper look at the reactive core you have been using all along.