Vue.js 35 🟢 Named Slots and Slot Shorthand Syntax (#)
A component’s content is not always a single block. A layout component has a header, a body, and a footer. A card has a title, an image, and a description. A modal has a header, a body, and the action buttons. The default slot receives one block of content, and the component decides where it goes. But when the component needs to place different parts of the content in different positions, a single slot is not enough. The named slots solve this: each slot has a name, and the parent targets the content at the slot by name. The default slot is the unnamed one, and the named slots are the additional positions.
The parent targets a named slot with the <template> element and the v-slot directive: <template v-slot:header>...</template>. The shorthand for the directive is the # character: <template #header>...</template>. The shorthand is the modern form, and it is what the Vue documentation and the community use. The v-slot directive can only be used on the <template> element or on a component, and the shorthand # follows the same rule. The named slots can be combined with the default slot, and the default slot’s content is the content that is not wrapped in a <template> with a name.
The named slots have several rules. The order of the <template> elements in the parent’s template does not matter; the component renders the slots in the order they appear in its own template, not in the order the parent passes them. The named slot’s name must match the name attribute of the <slot> in the component. The # shorthand is the equivalent of the v-slot:, and the two can be mixed. The default slot can be targeted with #default or with the content that is not wrapped in a <template>.
This chapter covers three areas. First, why the named slots exist — the multiple-content problem and the named-slot solution. Second, how the named slots and the shorthand work — the <template #name> syntax, the <slot name="name"> in the component, the default slot’s interaction, and the dynamic slot names. Third, the advanced patterns — the slot props, the conditional rendering of the named slots, the component’s inspection of the $slots, and the combination with the other features. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the named slot flow.
Key point: The named slots let the parent pass the content to the specific positions in the component. The parent uses <template #name>...</template>, and the component uses <slot name="name" />. The shorthand # is the equivalent of the v-slot:. The default slot is the unnamed one, and it receives the content that is not wrapped in a named <template>. The order of the parent’s <template> elements does not affect the render order.
Why named slots exist
The multiple-content problem. A component has more than one content position. The header is at the top, the body is in the middle, and the footer is at the bottom. The default slot receives one block of content, and the component places it in one position. The other positions have no content. The named slots solve the problem by giving each position a name, and the parent targets the content at the position by the name.
The placement problem. The component decides where the content goes. The header slot is rendered in the <header> element, the body slot in the <main>, and the footer slot in the <footer>. The parent does not know the structure; it only knows the names. The component can change the structure without affecting the parent, and the parent can change the content without affecting the component. The names are the contract, and the positions are the implementation.
The optional-content problem. Not every named slot is filled. A card with no footer has no footer content, and the component can conditionally render the footer wrapper. The $slots.footer check tells the component whether the parent passed the content. The conditional rendering prevents the empty wrappers from appearing in the DOM.
The readability problem. The named slots make the parent’s template readable. The <template #header>...</template> explicitly says “this content goes to the header.” The alternative — a single default slot with the content that the component parses — is fragile and implicit. The named slots are the declarative approach.
The shorthand problem. The v-slot:header directive is verbose. The #header shorthand is the compact equivalent. The Vue team introduced the shorthand in Vue 2.6, and it is the recommended form. The # is the single character that replaces the v-slot:, and the rest of the syntax is the same.
The trade-off. The named slots add the verbosity to the parent’s template. A card with the three slots has the three <template> elements, and the parent’s template is longer. The alternative — a single default slot — is shorter but less explicit. The trade-off is between the verbosity of the named slots and the implicitness of the single slot. For the components with the multiple positions, the named slots are the better choice.
a. The named slots
A named slot is declared in the component with the name attribute on the <slot> element.
<!-- Card.vue -->
<template>
<div class="card">
<div class="card__header">
<slot name="header" />
</div>
<div class="card__body">
<slot />
</div>
<div class="card__footer">
<slot name="footer" />
</div>
</div>
</template>
The header and the footer are the named slots, and the <slot /> without the name is the default slot. The component renders the three slots in the order they appear in its template.
The parent targets the named slots with the <template> elements:
<template>
<Card>
<template #header>
<h2>Card Title</h2>
</template>
<p>This is the card body.</p>
<template #footer>
<button>Close</button>
</template>
</Card>
</template>
The #header and the #footer are the shorthands for the v-slot:header and the v-slot:footer. The <p> is not wrapped in a <template>, so it goes to the default slot. The rendered HTML has the header content in the card__header, the body content in the card__body, and the footer content in the card__footer.
The order does not matter. The parent can put the <template #footer> before the <template #header>, and the component still renders the header first, because the component’s template determines the order.
<template>
<Card>
<template #footer>
<button>Close</button>
</template>
<template #header>
<h2>Card Title</h2>
</template>
<p>This is the card body.</p>
</Card>
</template>
The rendered output is the same. The component’s template is the source of the order.
The v-slot directive. The v-slot:header is the long form of the #header. The two are equivalent, and the choice is a matter of the preference. The v-slot can only be used on the <template> or on the component itself.
<template v-slot:header>
<h2>Card Title</h2>
</template>
The default slot. The content that is not wrapped in a named <template> goes to the default slot. The default slot can also be targeted explicitly with the #default:
<template #default>
<p>This is the card body.</p>
</template>
The #default is the explicit form, and the unwrapped content is the implicit form. The two produce the same result.
b. The shorthand and the dynamic names
The shorthand. The # is the shorthand for the v-slot:. The #header is the v-slot:header, and the #default is the v-slot:default. The shorthand is the recommended form, and it is what the Vue documentation uses.
<template #header>...</template>
<template #footer>...</template>
<template #default>...</template>
The dynamic slot names. The slot name can be dynamic with the v-slot:[name] syntax. The name is a JavaScript expression, and it is evaluated in the parent’s scope.
<script setup>
import { ref } from 'vue';
const slotName = ref('header');
</script>
<template>
<Card>
<template #[slotName]>
<h2>Dynamic Header</h2>
</template>
</Card>
</template>
The #[slotName] is the dynamic slot name. The slotName is the parent’s ref, and the slot content is placed in the slot with the matching name. The dynamic names are useful when the slot is determined at runtime.
The v-slot on the component. The v-slot can be used on the component itself, and the content goes to the default slot. The v-slot:header on the component is not valid; the named slots must be on the <template>.
<!-- ✅ The default slot -->
<Card v-slot="{ item }">
{{ item }}
</Card>
<!-- ❌ The named slot on the component -->
<Card v-slot:header>...</Card>
The first form is the scoped slot for the default slot, and the second is not valid. The named slots must use the <template> element.
The mix of the named and the default. The parent can use the named <template> elements and the unwrapped content together. The unwrapped content goes to the default slot, and the <template> elements go to the named slots.
<Card>
<template #header>
<h2>Title</h2>
</template>
<p>Body content</p>
<template #footer>
<button>Close</button>
</template>
</Card>
The <p> is the default slot content, and the two <template> elements are the named slots.
c. The advanced patterns
The conditional rendering of the named slots. The component checks the $slots object and renders the wrapper only when the slot is filled.
<!-- 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 v-if="$slots.header" prevents the empty card__header from being rendered when the parent passes no header content. The $slots.footer does the same for the footer.
The useSlots() composable. The useSlots() returns the $slots object in the <script setup>, and the component can use the computed properties to check the slots.
<script setup>
import { computed, useSlots } from 'vue';
const slots = useSlots();
const hasHeader = computed(() => !!slots.header);
const hasFooter = computed(() => !!slots.footer);
</script>
<template>
<div class="card">
<div v-if="hasHeader" class="card__header">
<slot name="header" />
</div>
<div class="card__body">
<slot />
</div>
<div v-if="hasFooter" class="card__footer">
<slot name="footer" />
</div>
</div>
</template>
The hasHeader and the hasFooter are the computed properties that check the slots. The template uses them in the v-if directives. The pattern is the same as the $slots in the template, but it is more readable when the checks are complex.
The scoped slots with the named slots. The named slots can have the slot props, which expose the component’s data to the parent’s template. The parent receives the props with the v-slot:name="props" or the #name="props".
<!-- List.vue -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot name="item" :item="item" :index="index" />
</li>
</ul>
</template>
<script setup>
defineProps({
items: Array,
});
</script>
The parent receives the item and the index:
<List :items="items">
<template #item="{ item, index }">
<span>{{ index }}: {{ item.name }}</span>
</template>
</List>
The #item="{ item, index }" destructures the slot props. The pattern is the scoped slot with the named slot, and it is the way the component passes the data to the parent’s template.
The dynamic slot names with the scoped props. The dynamic name and the scoped props can be combined:
<template #[dynamicName]="{ item }">
{{ item }}
</template>
The dynamicName is the parent’s expression, and the { item } is the slot props.
Complete Example Session
<!-- ============================================
PART 1: THE NAMED SLOTS IN THE COMPONENT
============================================ -->
<!-- Card.vue -->
<template>
<div class="card">
<div class="card__header">
<slot name="header" />
</div>
<div class="card__body">
<slot />
</div>
<div class="card__footer">
<slot name="footer" />
</div>
</div>
</template>
<!-- ============================================
PART 2: THE PARENT USING THE NAMED SLOTS
============================================ -->
<template>
<Card>
<template #header>
<h2>Card Title</h2>
</template>
<p>This is the card body.</p>
<template #footer>
<button>Close</button>
</template>
</Card>
</template>
<!-- The #header and the #footer are the shorthands. -->
<!-- The <p> goes to the default slot. -->
<!-- ============================================
PART 3: THE ORDER DOES NOT MATTER
============================================ -->
<template>
<Card>
<template #footer>
<button>Close</button>
</template>
<template #header>
<h2>Card Title</h2>
</template>
<p>This is the card body.</p>
</Card>
</template>
<!-- The rendered output is the same. -->
<!-- The component's template determines the order. -->
<!-- ============================================
PART 4: THE v-slot LONG FORM
============================================ -->
<template>
<Card>
<template v-slot:header>
<h2>Card Title</h2>
</template>
<template v-slot:footer>
<button>Close</button>
</template>
</Card>
</template>
<!-- The v-slot:header is equivalent to the #header. -->
<!-- ============================================
PART 5: THE EXPLICIT DEFAULT SLOT
============================================ -->
<template>
<Card>
<template #header>
<h2>Title</h2>
</template>
<template #default>
<p>Body content</p>
</template>
</Card>
</template>
<!-- The #default is the explicit default slot. -->
<!-- ============================================
PART 6: THE CONDITIONAL NAMED 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 v-if prevents the empty wrappers. -->
<!-- ============================================
PART 7: THE useSlots COMPOSABLE
============================================ -->
<script setup>
import { computed, useSlots } from 'vue';
const slots = useSlots();
const hasHeader = computed(() => !!slots.header);
const hasFooter = computed(() => !!slots.footer);
</script>
<template>
<div class="card">
<div v-if="hasHeader" class="card__header">
<slot name="header" />
</div>
<div class="card__body">
<slot />
</div>
<div v-if="hasFooter" class="card__footer">
<slot name="footer" />
</div>
</div>
</template>
<!-- ============================================
PART 8: THE DYNAMIC SLOT NAMES
============================================ -->
<script setup>
import { ref } from 'vue';
const slotName = ref('header');
</script>
<template>
<Card>
<template #[slotName]>
<h2>Dynamic Header</h2>
</template>
</Card>
</template>
<!-- The #[slotName] is the dynamic slot name. -->
<!-- ============================================
PART 9: THE SCOPED NAMED SLOTS
============================================ -->
<!-- List.vue -->
<template>
<ul>
<li v-for="(item, index) in items" :key="item.id">
<slot name="item" :item="item" :index="index" />
</li>
</ul>
</template>
<script setup>
defineProps({
items: Array,
});
</script>
<!-- The parent -->
<template>
<List :items="items">
<template #item="{ item, index }">
<span>{{ index }}: {{ item.name }}</span>
</template>
</List>
</template>
<!-- ============================================
PART 10: THE COMPLETE LAYOUT COMPONENT
============================================ -->
<!-- AppLayout.vue -->
<script setup>
import { useSlots } from 'vue';
const slots = useSlots();
</script>
<template>
<div class="layout">
<header v-if="slots.header" class="layout__header">
<slot name="header" />
</header>
<aside v-if="slots.sidebar" class="layout__sidebar">
<slot name="sidebar" />
</aside>
<main class="layout__main">
<slot />
</main>
<footer v-if="slots.footer" class="layout__footer">
<slot name="footer" />
</footer>
</div>
</template>
<!-- The parent -->
<template>
<AppLayout>
<template #header>
<nav>...</nav>
</template>
<template #sidebar>
<ul>...</ul>
</template>
<article>
<h1>Main content</h1>
</article>
<template #footer>
<p>© 2024</p>
</template>
</AppLayout>
</template>
The ten parts show the named slots in the component, the parent using them, the order independence, the v-slot long form, the explicit default slot, the conditional named slots, the useSlots composable, the dynamic slot names, the scoped named slots, and the complete layout component.
Quick Reference
The Named Slot Syntax
| In the component | In the parent |
|---|---|
<slot name="header" /> | <template #header>...</template> |
<slot name="footer" /> | <template #footer>...</template> |
<slot /> | The unwrapped content |
<slot name="default" /> | <template #default>...</template> |
The Shorthand
| Long form | Shorthand |
|---|---|
v-slot:header | #header |
v-slot:footer | #footer |
v-slot:default | #default |
v-slot:[name] | #[name] |
The Slot Props
| In the component | In the parent |
|---|---|
<slot name="item" :item="item" /> | <template #item="{ item }"> |
<slot :item="item" /> | <Component v-slot="{ item }"> |
The $slots Checks
| Check | Meaning |
|---|---|
$slots.header | The header slot is filled |
$slots.footer | The footer slot is filled |
$slots.default | The default slot is filled |
useSlots().header | The same in the script |
The Order
| The parent’s order | The rendered order |
|---|---|
#header, then #footer | The component’s order |
#footer, then #header | The component’s order |
| The order does not matter | The component’s template determines it |
Best Practices
✅ Do This:
<template>
<!-- Use the # shorthand -->
<template #header>...</template> // ✅
<!-- Check the slot before the wrapper -->
<div v-if="$slots.header" class="header"><slot name="header" /></div> // ✅
<!-- Use the explicit #default when the default is mixed -->
<template #default>...</template> // ✅
<!-- Use the dynamic names for the runtime slots -->
<template #[slotName]>...</template> // ✅
<!-- Use the scoped props for the data -->
<template #item="{ item }">...</template> // ✅
</template>
❌ Don’t Do This:
<template>
<!-- Don't use the v-slot:name on the component -->
<Card v-slot:header>...</Card> // not valid // ❌
<!-- Don't rely on the parent's order -->
// The component's template determines the order // ✅
<!-- Don't forget the wrapper's conditional -->
<div class="header"><slot name="header" /></div> // the empty div // ❌
<!-- Don't use the same name for the two slots -->
<slot name="header" /><slot name="header" /> // duplicated // ❌
<!-- Don't mix the default slot with the named slot wrongly -->
<template #header>...</template> <template #header>...</template> // ❌
</template>
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The named slot is not filled | The wrong #name | Check the name |
| The default slot is empty | The content is in a <template> | Move it out |
| The empty wrapper is rendered | No v-if | Check the $slots |
The v-slot:name on the component | The wrong placement | Use the <template> |
| The order is wrong | The parent’s order | The component’s template determines it |
| The dynamic name is not reactive | The wrong expression | Use the #[expression] |
| The scoped props are undefined | The slot does not pass them | Check the :prop |
Real-World Examples
1. The Card
<Card>
<template #header><h2>Title</h2></template>
<p>Body</p>
<template #footer><button>Close</button></template>
</Card>
2. The Layout
<AppLayout>
<template #header><nav>...</nav></template>
<article>...</article>
<template #footer><p>© 2024</p></template>
</AppLayout>
3. The Modal
<Modal>
<template #header><h2>Confirm</h2></template>
<p>Are you sure?</p>
<template #footer>
<button>Cancel</button>
<button>Confirm</button>
</template>
</Modal>
4. The Shorthand
<template #header>...</template>
5. The Explicit Default
<template #default>...</template>
6. The Conditional
<div v-if="$slots.header" class="header"><slot name="header" /></div>
7. The useSlots
const slots = useSlots();
8. The Dynamic Name
<template #[slotName]>...</template>
9. The Scoped Named Slot
<template #item="{ item, index }">{{ index }}: {{ item.name }}</template>
10. The Complete Layout
<AppLayout>
<template #header>...</template>
<template #sidebar>...</template>
<main>...</main>
<template #footer>...</template>
</AppLayout>
Visual
The Named Slot Flow
┌──────────────────────────────────────────────────────────────┐
│ THE NAMED SLOT FLOW │
│ │
│ Parent: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ <Card> │ │
│ │ <template #header> │ │
│ │ <h2>Card Title</h2> │ │
│ │ </template> │ │
│ │ <p>Body content</p> │ │
│ │ <template #footer> │ │
│ │ <button>Close</button> │ │
│ │ </template> │ │
│ │ </Card> │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ │ The content is routed to the slots by name. │
│ ▼ │
│ Card: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ <div class="card"> │ │
│ │ <div class="card__header"> │ │
│ │ <slot name="header" /> ← the header content │ │
│ │ </div> │ │
│ │ <div class="card__body"> │ │
│ │ <slot /> ← the default content │ │
│ │ </div> │ │
│ │ <div class="card__footer"> │ │
│ │ <slot name="footer" /> ← the footer content │ │
│ │ </div> │ │
│ │ </div> │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Order Independence
┌──────────────────────────────────────────────────────────────┐
│ THE ORDER INDEPENDENCE │
│ │
│ Parent order 1: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ <template #header>...</template> │ │
│ │ <p>Body</p> │ │
│ │ <template #footer>...</template> │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Parent order 2: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ <template #footer>...</template> │ │
│ │ <template #header>...</template> │ │
│ │ <p>Body</p> │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The rendered output is the SAME. │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The component's template determines the order: │ │
│ │ header → body → footer │ │
│ │ │ │
│ │ The parent's order does not matter. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Conditional Named Slots
┌──────────────────────────────────────────────────────────────┐
│ THE CONDITIONAL NAMED SLOTS │
│ │
│ WITHOUT the v-if: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ <div class="card__header"> │ │
│ │ <slot name="header" /> ← empty │ │
│ │ </div> │ │
│ │ │ │
│ │ The empty div is rendered. │ │
│ │ The padding, the border, the margin are applied. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ WITH the v-if: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ <div v-if="$slots.header" class="card__header"> │ │
│ │ <slot name="header" /> │ │
│ │ </div> │ │
│ │ │ │
│ │ The div is rendered only when the slot is filled. │ │
│ │ No empty wrappers in the DOM. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
The Scoped Named Slot
┌──────────────────────────────────────────────────────────────┐
│ THE SCOPED NAMED SLOT │
│ │
│ List.vue: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ <ul> │ │
│ │ <li v-for="(item, index) in items" :key="item.id">│ │
│ │ <slot name="item" :item="item" :index="index" />│ │
│ │ </li> │ │
│ │ </ul> │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ │ The slot passes the item and the index. │
│ ▼ │
│ Parent: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ <List :items="items"> │ │
│ │ <template #item="{ item, index }"> │ │
│ │ <span>{{ index }}: {{ item.name }}</span> │ │
│ │ </template> │ │
│ │ </List> │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ The component's data flows to the parent's template.│ │
│ │ The parent's template renders the data. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Named slot | <slot name="header" /> |
| The parent’s target | <template #header>...</template> |
| The shorthand | # for the v-slot: |
| The default slot | The unwrapped content |
| The explicit default | <template #default>...</template> |
| The order | The component’s template determines it |
| The conditional | v-if="$slots.header" |
| The dynamic name | #[expression] |
| The scoped props | <slot name="item" :item="item" /> |
The useSlots() | The composable for the script |
Key takeaways:
- The named slots let the parent pass the content to the specific positions in the component. The component declares the slots with the
nameattribute, and the parent targets them with the<template #name>elements. The default slot is the unnamed one and receives the unwrapped content . - The
#is the shorthand for thev-slot:. The#headeris thev-slot:header, and the#defaultis thev-slot:default. The shorthand is the recommended form, and it is what the Vue documentation uses . - The order of the parent’s
<template>elements does not affect the render order. The component’s template determines the order. The parent can put the#footerbefore the#header, and the component still renders the header first . - The default slot can be targeted with the
#default. The unwrapped content goes to the default slot, and the explicit#defaultis the equivalent. The two forms can be used together, and the explicit form is useful when the parent mixes the named and the default slots . - The component can check the
$slotsobject to conditionally render the slot wrappers. Thev-if="$slots.header"prevents the empty wrappers from appearing in the DOM. TheuseSlots()composable returns the same object in the script . - The slot names can be dynamic with the
#[expression]. The expression is evaluated in the parent’s scope, and the content is placed in the slot with the matching name. The dynamic names are useful when the slot is determined at runtime . - The named slots can have the scoped props. The
<slot name="item" :item="item" />passes the component’s data to the parent’s template, and the parent receives it with the#item="{ item }". The combination is the scoped named slot . - The
v-slotcan only be used on the<template>or on the component. Thev-slot:headeron the component is not valid; the named slots must use the<template>element. Thev-sloton the component is for the default slot’s scoped props .
Remember: The named slots let the parent pass the content to the specific positions in the component. The parent uses the <template #name>...</template>, and the component uses the <slot name="name" />. The # is the shorthand for the v-slot:, and the default slot receives the unwrapped content. The order of the parent’s <template> elements does not matter; the component’s template determines the render order. The component can check the $slots to conditionally render the wrappers, and the slot names can be dynamic. The scoped named slots pass the component’s data to the parent’s template. The result is a component that lets the parent fill the multiple content positions with the explicit, readable, and flexible API.
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!