Vue.js 34 🟢 Content Distribution with Unnamed Default
A component’s template defines its structure, but not all of its content. A button defines the <button> element, but the text inside comes from the parent. A card defines the border, the shadow, and the padding, but the content — the title, the body, the footer — comes from the parent. The <slot> element is the placeholder that marks where the parent’s content goes. It is the mechanism for content distribution: the parent provides the content, the component provides the structure, and the slot is the point where the two meet.
The default slot is the unnamed one. It is the slot that receives the content that the parent passes between the component’s opening and closing tags. A component with a single <slot /> in its template receives all of the parent’s child content at that position. The content can be text, HTML elements, other components, or a combination. The component does not know what the content is, and the parent does not know where the slot is. The two are decoupled, and the slot is the contract between them.
The default slot is the most common form, and it covers the majority of the use cases. A button, a card, a modal, a tooltip, and a layout container all use the default slot. The named slots and the scoped slots are the extensions for the cases where the component needs to place the content in multiple positions or pass the data back to the parent. The default slot is the foundation, and the named and the scoped slots build on it.
This chapter covers three areas. First, why the default slot exists — the content-distribution problem and the decoupling of the structure and the content. Second, how the default slot works — the <slot> element, the fallback content, the render scope, and the compilation to the render function. Third, the patterns and the edge cases — the fallback content, the multiple root elements, the $slots object, and the difference between the slot content and the props. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the slot flow.
Key point: The default slot is the unnamed <slot> element in a component’s template. It receives the content that the parent passes between the component’s tags. The slot content is compiled in the parent’s scope, so it has access to the parent’s data. The fallback content is the content between the <slot> tags, and it is rendered when the parent passes no content. The $slots object exposes the slots in the script.
Why the default slot exists
The content-distribution problem. A component defines a structure: a <button> with the styling, a <div> with the layout, a <dialog> with the behavior. The content inside the structure varies: the button’s label, the card’s body, the modal’s message. If the component hardcoded the content, it would be usable for one case only. If the component passed the content as a prop, the prop would be a string, and the parent could not pass the HTML or the other components. The slot solves the problem: the component defines the structure, and the parent provides the content, which can be anything.
The decoupling problem. The component and the parent are decoupled by the slot. The component does not know what the content is, and the parent does not know where the slot is in the component’s template. The component can change its internal structure without affecting the parent, and the parent can change the content without affecting the component. The slot is the contract, and the contract is the position, not the content.
The composition problem. The slot content is compiled in the parent’s scope. It has access to the parent’s data, the parent’s methods, and the parent’s other components. This is what makes the composition work: the parent passes <UserAvatar :user="currentUser" /> as the slot content, and the currentUser is the parent’s data. The component that defines the slot does not need to know about the UserAvatar or the currentUser. The parent’s scope is the content’s scope, and the component’s scope is the structure’s scope.
The fallback problem. A slot can have the fallback content, which is rendered when the parent passes no content. The fallback is the default that makes the component usable without the slot content. A button with the fallback Submit is usable without the parent passing the label, and the parent can override it with the custom content. The fallback is the sensible default, and the parent’s content is the override.
The multiple-content problem. A component can have more than one slot, and the default slot is the one without a name. The other slots are the named slots, and the parent targets them with the v-slot directive or the # shorthand. The default slot receives the content that is not targeted at a named slot. The named slots and the default slot together are the component’s content API.
The trade-off. The slot content is compiled in the parent’s scope, which means the component cannot access the parent’s data from the slot content. If the component needs to pass the data to the slot content, it uses the scoped slots, which are the slots that expose the component’s data to the parent’s template. The default slot without the scope is the simple case, and the scoped slots are the extension for the cases where the component needs to share the data.
a. The default slot
A component with a single <slot /> in its template receives the parent’s child content at that position.
<!-- MyButton.vue -->
<template>
<button class="btn">
<slot />
</button>
</template>
The parent uses the component and passes the content:
<template>
<MyButton>Submit</MyButton>
</template>
The rendered HTML has the content inside the button:
<button class="btn">Submit</button>
The Submit is the slot content, and the <slot /> is the placeholder. The component does not know the content is Submit, and the parent does not know the <button> is the structure.
The content can be anything. The slot content can be the text, the HTML, the other components, or the combination.
<template>
<MyButton>
<span class="icon">✓</span>
Submit
</MyButton>
</template>
The content is an icon and the text, and the slot receives both. The component renders them inside the button.
The content is compiled in the parent’s scope. The slot content has access to the parent’s data.
<script setup>
import { ref } from 'vue';
const count = ref(0);
</script>
<template>
<MyButton>Count: {{ count }}</MyButton>
</template>
The count is the parent’s data, and the slot content uses it. The MyButton component does not know about the count. The content is rendered with the parent’s data, and the component’s structure is rendered with the component’s data.
The render scope. The slot content is rendered in the parent’s scope, and the component’s template is rendered in the component’s scope. The two scopes are separate. The slot content cannot access the component’s data directly, and the component’s template cannot access the parent’s data directly. The props are the mechanism for the data to flow from the parent to the component, and the scoped slots are the mechanism for the data to flow from the component to the slot content.
b. The fallback content
The content between the <slot> tags is the fallback, and it is rendered when the parent passes no content.
<!-- MyButton.vue -->
<template>
<button class="btn">
<slot>Submit</slot>
</button>
</template>
The parent uses the component without the content:
<template>
<MyButton />
</template>
The rendered HTML has the fallback:
<button class="btn">Submit</button>
The fallback is the Submit, and it is rendered because the parent passed no content. The parent can override it:
<template>
<MyButton>Cancel</MyButton>
</template>
The rendered HTML has the parent’s content:
<button class="btn">Cancel</button>
The fallback is the default, and the parent’s content is the override.
The fallback can be the complex content. The fallback is the slot content, so it can be the text, the HTML, or the other components.
<template>
<button class="btn">
<slot>
<span class="icon">✓</span>
Submit
</slot>
</button>
</template>
The fallback is the icon and the text, and the parent can override it with the custom content.
The fallback and the empty content. The fallback is rendered when the parent passes no content. If the parent passes the empty string or the whitespace, the fallback is not rendered, because the content is present. The distinction matters when the parent conditionally passes the content.
c. The $slots object
The $slots object exposes the slots in the template and the script. The $slots.default is the default slot, and it is the function that returns the slot content.
<script setup>
import { useSlots } from 'vue';
const slots = useSlots();
const hasDefaultSlot = computed(() => !!slots.default);
</script>
<template>
<div class="card">
<div v-if="$slots.header" class="card__header">
<slot name="header" />
</div>
<div class="card__body">
<slot />
</div>
<div v-if="$slots.footer" class="card__footer">
<slot name="footer" />
</div>
</div>
</template>
The $slots.header is the named slot, and the $slots.default is the default. The v-if="$slots.header" checks whether the parent passed the header content, and the <slot name="header" /> renders it. The $slots object is the way the component knows which slots the parent filled.
The useSlots() composable. The useSlots() composable returns the $slots object in the <script setup>. The object is the same as the template $slots, and it is useful when the component needs to check the slots or render them programmatically.
<script setup>
import { useSlots } from 'vue';
const slots = useSlots();
</script>
The $slots.default(). The $slots.default is a function, and calling it returns the slot content’s VNodes. The component can render it conditionally or pass it to the other components.
<script setup>
import { useSlots, h } from 'vue';
const slots = useSlots();
</script>
<template>
<div>
<component :is="() => slots.default?.()" v-if="slots.default" />
<p v-else>No content</p>
</div>
</template>
The pattern is uncommon, but it is the way the component can inspect and render the slot content programmatically.
Complete Example Session
<!-- ============================================
PART 1: THE BASIC DEFAULT SLOT
============================================ -->
<!-- MyButton.vue -->
<template>
<button class="btn">
<slot />
</button>
</template>
<!-- The parent -->
<template>
<MyButton>Submit</MyButton>
</template>
<!-- The rendered HTML:
<button class="btn">Submit</button> -->
<!-- ============================================
PART 2: THE SLOT CONTENT WITH THE HTML
============================================ -->
<template>
<MyButton>
<span class="icon">✓</span>
Submit
</MyButton>
</template>
<!-- The slot receives the icon and the text. -->
<!-- ============================================
PART 3: THE SLOT CONTENT WITH THE PARENT'S DATA
============================================ -->
<script setup>
import { ref } from 'vue';
const count = ref(0);
</script>
<template>
<MyButton>Count: {{ count }}</MyButton>
</template>
<!-- The count is the parent's data.
The slot content is compiled in the parent's scope. -->
<!-- ============================================
PART 4: THE FALLBACK CONTENT
============================================ -->
<!-- MyButton.vue -->
<template>
<button class="btn">
<slot>Submit</slot>
</button>
</template>
<!-- The parent without the content -->
<template>
<MyButton />
</template>
<!-- The rendered HTML:
<button class="btn">Submit</button> -->
<!-- ============================================
PART 5: THE FALLBACK OVERRIDDEN
============================================ -->
<template>
<MyButton>Cancel</MyButton>
</template>
<!-- The rendered HTML:
<button class="btn">Cancel</button> -->
<!-- ============================================
PART 6: THE CARD COMPONENT
============================================ -->
<!-- Card.vue -->
<template>
<div class="card">
<div class="card__body">
<slot />
</div>
</div>
</template>
<!-- The parent -->
<template>
<Card>
<h2>Title</h2>
<p>Body text</p>
</Card>
</template>
<!-- ============================================
PART 7: THE NAMED AND THE DEFAULT SLOTS
============================================ -->
<!-- Card.vue -->
<template>
<div class="card">
<div v-if="$slots.header" class="card__header">
<slot name="header" />
</div>
<div class="card__body">
<slot />
</div>
<div v-if="$slots.footer" class="card__footer">
<slot name="footer" />
</div>
</div>
</template>
<!-- The parent -->
<template>
<Card>
<template #header>
<h2>Title</h2>
</template>
<p>Body text</p>
<template #footer>
<button>Close</button>
</template>
</Card>
</template>
<!-- ============================================
PART 8: THE useSlots COMPOSABLE
============================================ -->
<script setup>
import { computed, useSlots } from 'vue';
const slots = useSlots();
const hasDefault = computed(() => !!slots.default);
const hasHeader = computed(() => !!slots.header);
</script>
<template>
<div class="card">
<div v-if="hasHeader" class="card__header">
<slot name="header" />
</div>
<div class="card__body">
<slot />
</div>
</div>
</template>
<!-- ============================================
PART 9: THE CONDITIONAL SLOT RENDERING
============================================ -->
<!-- Alert.vue -->
<template>
<div class="alert" :class="`alert--${type}`">
<div v-if="$slots.icon" class="alert__icon">
<slot name="icon" />
</div>
<div class="alert__content">
<slot />
</div>
</div>
</template>
<script setup>
defineProps({
type: {
type: String,
default: 'info',
},
});
</script>
<!-- The parent -->
<template>
<Alert type="warning">
<template #icon>⚠</template>
This is a warning message.
</Alert>
</template>
<!-- ============================================
PART 10: THE COMPLETE COMPONENT
============================================ -->
<!-- Modal.vue -->
<script setup>
import { useSlots } from 'vue';
defineProps({
title: String,
});
const emit = defineEmits(['close']);
const slots = useSlots();
</script>
<template>
<div class="modal-overlay" @click.self="emit('close')">
<div class="modal">
<div class="modal__header">
<h2>{{ title }}</h2>
<button class="modal__close" @click="emit('close')">×</button>
</div>
<div class="modal__body">
<slot />
</div>
<div v-if="slots.footer" class="modal__footer">
<slot name="footer" />
</div>
</div>
</div>
</template>
<!-- The parent -->
<template>
<Modal title="Confirm" @close="handleClose">
<p>Are you sure you want to continue?</p>
<template #footer>
<button @click="handleClose">Cancel</button>
<button @click="handleConfirm">Confirm</button>
</template>
</Modal>
</template>
The ten parts show the basic default slot, the slot content with the HTML, the slot content with the parent’s data, the fallback content, the fallback overridden, the card component, the named and the default slots, the useSlots composable, the conditional slot rendering, and the complete component.
Quick Reference
The Default Slot
| Syntax | Meaning |
|---|---|
<slot /> | The default slot placeholder |
<slot>fallback</slot> | The default slot with the fallback |
<MyComponent>content</MyComponent> | The parent passes the content |
$slots.default | The default slot in the template |
useSlots().default | The default slot in the script |
The Slot Content
| Content | Where the scope |
|---|---|
| The text | The parent |
| The HTML | The parent |
| The components | The parent |
| The parent’s data | The parent |
| The component’s data | Not directly (use the scoped slots) |
The Fallback
| Situation | Rendered |
|---|---|
| No content | The fallback |
| The empty string | The empty string (no fallback) |
| The whitespace | The whitespace (no fallback) |
| The content | The content |
The $slots Object
| Property | Meaning |
|---|---|
$slots.default | The default slot’s function |
$slots.name | The named slot’s function |
$slots.default() | The default slot’s VNodes |
v-if="$slots.name" | Check whether the slot is filled |
useSlots() | The composable for the script |
Best Practices
✅ Do This:
<template>
<!-- Use the default slot for the content -->
<button class="btn"><slot /></button> // ✅
<!-- Provide the fallback content -->
<button class="btn"><slot>Submit</slot></button> // ✅
<!-- Check the slot before the wrapper -->
<div v-if="$slots.header" class="header"><slot name="header" /></div> // ✅
<!-- Use useSlots for the script access -->
const slots = useSlots(); // ✅
<!-- Keep the slot content in the parent's scope -->
<MyButton>{{ parentData }}</MyButton> // ✅
</template>
❌ Don’t Do This:
<template>
<!-- Don't access the parent's data from the component -->
<slot>{{ parentData }}</slot> // parentData is not in the component // ❌
<!-- Don't use the slot content as the prop -->
<MyButton :label="'Submit'" /> // use the slot instead // ✅
<!-- Don't forget the slot in the component -->
<button class="btn"></button> // the content is lost // ❌
<!-- Don't render the slot wrapper when the slot is empty -->
<div class="header"><slot name="header" /></div> // the empty div // ❌
<!-- Don't use the default slot for the multiple positions -->
<slot /> <slot /> // use the named slots // ❌
</template>
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The content is not rendered | The <slot /> is missing | Add the slot |
| The fallback is not rendered | The whitespace content | Remove the whitespace |
| The parent’s data is not available | The slot content is in the component’s scope | The slot content is in the parent’s scope |
| The named slot is not filled | The wrong #name | Check the name |
| The slot is empty | The parent passed no content | Use the fallback |
The $slots is undefined | The wrong access | Use useSlots() in the script |
| The empty wrapper is rendered | No v-if | Check the slot before the wrapper |
Real-World Examples
1. The Basic Button
<template>
<button class="btn"><slot /></button>
</template>
2. The Button with the Fallback
<template>
<button class="btn"><slot>Submit</slot></button>
</template>
3. The Card
<template>
<div class="card"><slot /></div>
</template>
4. The Card with the Named Slots
<template>
<div class="card">
<div v-if="$slots.header" class="header"><slot name="header" /></div>
<div class="body"><slot /></div>
</div>
</template>
5. The Modal
<template>
<div class="modal">
<slot />
<div v-if="$slots.footer" class="footer"><slot name="footer" /></div>
</div>
</template>
6. The Alert
<template>
<div class="alert"><slot /></div>
</template>
7. The Slot with the HTML
<MyButton><span class="icon">✓</span> Submit</MyButton>
8. The Slot with the Parent’s Data
<MyButton>Count: {{ count }}</MyButton>
9. The useSlots
const slots = useSlots();
10. The Complete Modal
<Modal title="Confirm" @close="handleClose">
<p>Are you sure?</p>
<template #footer>
<button>Cancel</button>
<button>Confirm</button>
</template>
</Modal>
Visual
The Slot Flow
┌──────────────────────────────────────────────────────────────┐
│ THE SLOT FLOW │
│ │
│ Parent: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ <MyButton>Submit</MyButton> │ │
│ │ │ │
│ │ The "Submit" is the slot content. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ │ The content is passed to the component. │
│ ▼ │
│ MyButton: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ <template> │ │
│ │ <button class="btn"> │ │
│ │ <slot /> ← the content goes here │ │
│ │ </button> │ │
│ │ </template> │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ │ The slot is replaced with the content. │
│ ▼ │
│ The rendered HTML: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ <button class="btn">Submit</button> │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Render Scope
┌──────────────────────────────────────────────────────────────┐
│ THE RENDER SCOPE │
│ │
│ Parent scope: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ const count = ref(0) │ │
│ │ │ │
│ │ <MyButton>Count: {{ count }}</MyButton> │ │
│ │ └──────────────────┘ │ │
│ │ The content is compiled in the │ │
│ │ parent's scope. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Component scope: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ defineProps({ ... }) │ │
│ │ │ │
│ │ <button class="btn"> │ │
│ │ <slot /> │ │
│ │ </button> │ │
│ │ The structure is rendered in the component's scope. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The two scopes are separate. │ │
│ │ The slot content cannot access the component's data │ │
│ │ (use the scoped slots). │ │
│ │ The component cannot access the parent's data │ │
│ │ (use the props). │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Fallback
┌──────────────────────────────────────────────────────────────┐
│ THE FALLBACK │
│ │
│ The component: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ <button class="btn"> │ │
│ │ <slot>Submit</slot> │ │
│ │ </button> │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The parent without the content: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ <MyButton /> │ │
│ │ → <button class="btn">Submit</button> │ │
│ │ │ │
│ │ The fallback is rendered. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The parent with the content: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ <MyButton>Cancel</MyButton> │ │
│ │ → <button class="btn">Cancel</button> │ │
│ │ │ │
│ │ The fallback is overridden. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The $slots Object
┌──────────────────────────────────────────────────────────────┐
│ THE $slots OBJECT │
│ │
│ Card.vue: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ <div class="card"> │ │
│ │ <div v-if="$slots.header" class="header"> │ │
│ │ <slot name="header" /> │ │
│ │ </div> │ │
│ │ <div class="body"> │ │
│ │ <slot /> │ │
│ │ </div> │ │
│ │ </div> │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ $slots.default → the default slot's function │ │
│ │ $slots.header → the header slot's function │ │
│ │ │ │
│ │ v-if="$slots.header" │ │
│ │ → true if the parent passed the header content │ │
│ │ → false otherwise │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The $slots object is the way the component knows which │
│ slots the parent filled. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Default slot | The unnamed <slot /> |
| The content | The parent’s child content |
| The scope | The parent’s scope |
| The fallback | The content between the <slot> tags |
$slots.default | The default slot in the template |
useSlots() | The composable for the script |
| The named slots | The v-slot:name or the #name |
| The scoped slots | The slot props for the parent’s template |
Key takeaways:
- The default slot is the unnamed
<slot />in a component’s template. It receives the content that the parent passes between the component’s tags. The content can be the text, the HTML, the other components, or the combination . - The slot content is compiled in the parent’s scope. It has access to the parent’s data, the parent’s methods, and the parent’s other components. The component does not know what the content is, and the parent does not know where the slot is .
- The fallback content is the content between the
<slot>tags. It is rendered when the parent passes no content. The parent’s content overrides the fallback. The fallback is the sensible default that makes the component usable without the slot content . - The slot decouples the structure from the content. The component defines the structure, and the parent provides the content. The component can change its internal structure without affecting the parent, and the parent can change the content without affecting the component .
- The
$slotsobject exposes the slots in the template. The$slots.defaultis the default slot, and the$slots.nameis the named slot. Thev-if="$slots.name"checks whether the slot is filled, and the<slot name="name" />renders it . - The
useSlots()composable returns the$slotsobject in the script. The object is the same as the template$slots, and it is useful when the component needs to check the slots or render them programmatically . - The named slots and the default slot together are the component’s content API. The default slot receives the content that is not targeted at a named slot, and the named slots receive the content that is. The named slots use the
v-slot:namedirective or the#nameshorthand . - The scoped slots are the extension for the data flow from the component to the slot content. The default slot without the scope is the simple case, and the scoped slots are the mechanism for the component to pass the data to the parent’s template .
Remember: The default slot is the unnamed <slot /> that receives the parent’s child content. The content is compiled in the parent’s scope, and the fallback content is rendered when the parent passes no content. The $slots object exposes the slots in the template, and the useSlots() composable exposes them in the script. The slot decouples the structure from the content, and the named and the scoped slots are the extensions. The result is a component that defines the structure and lets the parent provide the content, with the fallback as the sensible default.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!