Slots and Content Distribution

intermediate
12 min

Slots and Content Distribution

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.

The Default Slot

A <slot> outlet inside a child marks where parent-provided content should render:

vue
<!-- BaseCard.vue --> <template> <div class="card"> <slot></slot> </div> </template> <style scoped> .card { border: 1px solid #ddd; border-radius: 8px; padding: 1rem; } </style>
vue
<!-- 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.

Fallback Content

Content between the <slot> tags renders only when the parent provides nothing:

vue
<!-- SubmitButton.vue --> <template> <button type="submit"><slot>Submit</slot></button> </template>

<SubmitButton /> shows "Submit"; <SubmitButton>Save draft</SubmitButton> shows the custom label.

Named Slots

Layout-style components need several insertion points. Give each <slot> a name, and fill them with <template #name>:

vue
<!-- PageLayout.vue --> <template> <header><slot name="header"></slot></header> <main><slot></slot></main> <footer><slot name="footer"></slot></footer> </template>
vue
<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.

Scoped Slots: Data Back Up Into Content

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:

vue
<!-- 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>
vue
<!-- 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 }">.

Checking Whether a Slot Was Provided

Occasionally a wrapper should skip markup when a slot is empty. $slots exposes provided slots in the template:

vue
<footer v-if="$slots.footer" class="card-footer"> <slot name="footer"></slot> </footer>

Key Takeaways

  • <slot> renders parent-supplied template content; fallback content sits between the tags.
  • Slot content sees the parent's scope, not the child's.
  • Named slots (<slot name="x"> + <template #x>) enable multi-region layouts.
  • Scoped slots pass child data to parent-defined templates -- ideal for reusable lists and tables.
  • Use $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.

Slots and Content Distribution - Vue.js | CodeYourCraft | CodeYourCraft