Vue Slots: The Complete Guide (Default, Named, Scoped)

Slots are how a Vue component accepts and renders content from its parent. The child defines a slot placeholder; the parent fills it at the point of use. Vue has three kinds: default slots for a single content hole, named slots for multiple holes, and scoped slots for passing data from the child back up to the parent's slot content.
Slots are one of Vue's most powerful features for building flexible, reusable components. They let the parent decide what to render while the child controls where and how it appears. Whether you're wrapping content in a card, building a layout, or customizing each row of a list, vue slots are the right tool. This guide walks you through the basics, advanced techniques, and plenty of practical scenarios you'll use every day.
Slot Type Comparison
Use this quick-reference matrix to understand when and how to use each slot type in Vue 3:
| Slot type | What it does | Use when | Syntax |
|---|---|---|---|
| Default | One unnamed content hole | Component wraps a single content block | <slot /> |
| Named | Multiple labelled holes | Layouts, cards (header/body/footer) | <slot name="header" /> • <template #header> |
| Scoped | Child exposes data to parent's content | Lists, tables, renderless components | <slot :item="item" /> • <template #default="{ item }"> |
| Dynamic Name | Slot name resolved at runtime | Slot chosen dynamically by a variable | <slot :name="dynamicName" /> • <template #[dynamicName]> |
Vue.js Slots Basics
What Are Vue.js Slots?
A slot is a placeholder in a child component that gets filled with content passed from the parent. The child defines one or more <slot> outlets; the parent provides the content. That keeps the child's structure and behavior (e.g. styling, logic) while giving the parent full control over the actual HTML content.
Think of a button: the child might handle type, disabled state, and click handling, but the label (maybe along with an icon) comes from the parent. Slots are how you pass that label (or any template fragment) in.
Default Slot for Vue.js Slots
The simplest form is a single, unnamed slot—the default slot. Anything you put between the child's opening and closing tags is passed into that slot.
Child component (ButtonSubmit.vue):
<script setup lang="ts">
// Handles submit logic, styling, etc.
</script>
<template>
<button type="submit" class="btn btn-primary">
<slot></slot>
</button>
</template>Parent usage:
<template>
<ButtonSubmit><IconSave />Save changes</ButtonSubmit>
</template>The text "Save changes" and the save icon is slotted into the button. You can pass any valid template elements, components, etc to the slot.
Fallback Content for Vue.js Slots
You can put content inside <slot> in the child. That content is used only when the parent does not provide anything for the slot. In other words, it's the slot's default.
<!-- ButtonSubmit.vue -->
<template>
<button type="submit" class="btn btn-primary">
<slot>Submit</slot>
</button>
</template>If the parent uses <ButtonSubmit></ButtonSubmit> with no content, "Submit" is shown. If the parent uses <ButtonSubmit>Save</ButtonSubmit>, "Save" is shown.
Slots and Template Props Patterns
Defining slot defaults with prop values is a common Vue component design pattern to progressively enhance a component's API. This makes for quick usage with the prop but maintains flexibility for more complex needs. For example, you could define a simple button component with a label prop along with a slot for a label:
<!-- Button.vue -->
<script setup lang="ts">
defineProps<{ label?: string }>();
</script>
<template>
<button type="submit" class="btn btn-primary">
<slot>{{ label }}</slot>
</button>
</template>This is exactly what popular UI libraries like Nuxt UI do with their components. (Don't believe me? Check out the source code for the Nuxt UI Button component for yourself.).
Named slots
When a component needs multiple distinct content areas, use named slots. The child gives each <slot> a name; the parent targets them with v-slot (or the # shorthand) on <template>.
Child component (Card.vue):
<script setup lang="ts">
defineProps<{ title?: string }>();
</script>
<template>
<article class="card">
<header class="card-header">
<slot name="header"></slot>
</header>
<div class="card-body">
<slot></slot>
</div>
<footer class="card-footer">
<slot name="footer"></slot>
</footer>
</article>
</template>The unnamed <slot> is the default slot. The parent fills each slot by name:
<template>
<Card>
<template #header>
<h2>Account settings</h2>
</template>
<template #default>
<p>Update your email and password.</p>
</template>
<template #footer>
<button>Save</button>
</template>
</Card>
</template>#headeris shorthand forv-slot:header.#defaulttargets the default slot. You can also put the main content directly between<Card>and</Card>without a wrapper; that goes to the default slot.
So: one default slot for the main content, plus as many named slots as you need (header, footer, sidebar, etc.).
Advanced Vue.js Slots Techniques
Scoped Slots (Slot Props)
Sometimes the child has data the parent needs to render the slot (e.g. each item in a list, or internal state). Scoped slots let the child pass props into the slot; the parent receives them and uses them in the template.
The child exposes data by binding attributes on <slot>:
<!-- Child: MyComponent.vue -->
<script setup lang="ts">
import { ref } from "vue";
const greetingMessage = ref("Hello from the child!");
</script>
<template>
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
</template>The parent receives a single object (e.g. slotProps) and uses it:
<!-- Parent: App.vue -->
<template>
<MyComponent v-slot="slotProps">
{{ slotProps.text }} — count: {{ slotProps.count }}
</MyComponent>
</template>When there is only a default slot and you need props, you can use v-slot directly on the component. When you have multiple slots or need to name the default one explicitly, use <template #default="slotProps">.
Destructuring Slot Props
You can destructure the props right in v-slot for cleaner template code:
<template>
<MyComponent v-slot="{ text, count }">
{{ text }} — count: {{ count }}
</MyComponent>
</template>You can also rename the props like this: v-slot="{ text: greeting, count }". This is useful for:
- Avoiding name collisions
- Clarifying the meaning of the props within the parent context
Named Scoped Slots
Named slots can also pass props. The child gives a slot a name and binds data; the parent uses the same name and receives the props:
Child:
<template>
<div>
<slot name="header" :title="pageTitle"></slot>
<slot :items="items"></slot>
<slot name="footer" :year="currentYear"></slot>
</div>
</template>Parent:
<template>
<MyComponent>
<template #header="{ title }">
<h1>{{ title }}</h1>
</template>
<template #default="{ items }">
<ul>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
</template>
<template #footer="{ year }">
<p>© {{ year }}</p>
</template>
</MyComponent>
</template>So: named slots define where content goes; scoped slots define what data the parent gets when rendering that content.
Mixing Explicit Default and Named Scoped Slots
When you have both a default scoped slot and other named slots, use an explicit default slot so the scope is clear:
<MyComponent>
<template #default="{ message }">
<p>{{ message }}</p>
</template>
<template #footer>
<p>Here's some contact info</p>
</template>
</MyComponent>If you put content between <MyComponent> and </MyComponent> without <template #default>, that content does not have access to the default slot's props. Always use <template #default="..."> when you need those props.
Passing Dynamic Slot Names
Slot names can be passed dynamically using the same bracket syntax as other directives:
<template>
<BaseLayout>
<template v-slot:[dynamicSlotName]>
Content for {{ dynamicSlotName }}
</template>
<!-- Shorthand -->
<template #[dynamicSlotName]> ... </template>
</BaseLayout>
</template>Defining Dynamic Slot Names
Besides passing slot names dynamically, you can even define them dynamically. I've found this especially useful for customizing table cells for particular columns.
<template>
<table>
<tbody>
<tr v-for="item in items" :key="item.id">
<td v-for="column in columns" :key="column.key">
<!-- Note the use of the `item-${column.key}` slot name -->
<slot :name="`item-${column.key}`" :item="item">
{{ item[column.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
</template>Conditional Slots
You can use v-if along with the $slots object to conditionally render other html elements based on whether the parent provided them or not. (Sometimes useful to control slot wrapper elements, but used rarely.)
<template>
<div v-if="$slots.header">
<slot name="header">
<h1>Default header</h1>
</slot>
</div>
</template>Practical Scenarios for Vue.js Slots
1. Card component (header, body, footer)
A reusable card is the perfect use case for slots. It can define a fixed structure and styling but leave the content fully controlled by the parent.
AppCard.vue:
<script setup lang="ts"></script>
<template>
<article class="rounded-lg border bg-card p-4 shadow-sm">
<header v-if="$slots.header" class="mb-3 border-b pb-2">
<slot name="header"></slot>
</header>
<div class="card-body">
<slot></slot>
</div>
<footer v-if="$slots.footer" class="mt-3 border-t pt-2">
<slot name="footer"></slot>
</footer>
</article>
</template>Using v-if="$slots.header" (and $slots.footer) keeps the header/footer DOM only when the parent actually provides those slots. Parent usage:
<AppCard>
<template #header>
<h3>Profile</h3>
</template>
<p>Your bio and preferences.</p>
<template #footer>
<button>Edit profile</button>
</template>
</AppCard>2. Page layout (header, sidebar, main, footer)
A layout component that reserves named regions so the parent can fill them per page.
AppLayout.vue:
<script setup lang="ts"></script>
<template>
<div class="app-layout">
<header v-if="$slots.header" class="app-header">
<slot name="header"></slot>
</header>
<div class="app-body">
<aside v-if="$slots.sidebar" class="app-sidebar">
<slot name="sidebar"></slot>
</aside>
<main class="app-main">
<slot></slot>
</main>
</div>
<footer v-if="$slots.footer" class="app-footer">
<slot name="footer"></slot>
</footer>
</div>
</template>Parent (e.g. a page):
<template>
<AppLayout>
<template #header>
<AppNav />
</template>
<template #sidebar>
<AppSidebar />
</template>
<h1>Dashboard</h1>
<p>Main page content here.</p>
<template #footer>
<p>© 2025 My App</p>
</template>
</AppLayout>
</template>3. Data table / list with customizable rows
The child fetches or holds the list and handles layout; the parent decides how each row looks using a scoped slot.
DataTable.vue:
<script setup>
defineProps({
items: {
type: Array,
required: true,
},
columns: {
type: Array,
required: true,
},
});
</script>
<template>
<table class="table">
<thead>
<tr>
<th v-for="column in columns" :key="column.key">{{ column.label }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in items" :key="JSON.stringify(item)">
<slot :name="`row-${index}`" :item="item">
<td v-for="column in columns" :key="column.key">
<slot :name="`item-${column.key}`" :item="item">
{{ item[column.key] }}
</slot>
</td>
</slot>
</tr>
</tbody>
</table>
</template>Parent:
<script setup lang="ts">
import MyTable from "./components/MyTable.vue";
import { ref } from "vue";
const tableData = ref({
columns: [
{ key: "name", label: "Name" },
{ key: "age", label: "Age" },
],
items: [
{ name: "John", age: 20 },
{ name: "Jane", age: 24 },
{ name: "Susan", age: 16 },
{ name: "Chris", age: 55 },
{ name: "Dan", age: 40 },
],
});
</script>
<template>
<MyTable :items="tableData.items" :columns="tableData.columns">
<template #row-0>
<td>hello</td>
<td>hello</td>
</template>
<template #item-name="{ item }">
<strong>{{ item.name }}</strong>
</template>
</MyTable>
</template>Same MyTable can be reused for products, orders, etc.—only the slot content changes.
4. Form wrapper with fields and actions
A form container that provides layout and submit state handling, with slots for the fields and the actions (buttons).
FormSection.vue:
<script setup lang="ts">
defineProps<{ title?: string; submit: () => void }>();
const isSubmitting = ref(false);
async function handleSubmit() {
isSubmitting.value = true;
await props.submit();
isSubmitting.value = false;
}
</script>
<template>
<form class="form-section" @submit.prevent="handleSubmit">
<fieldset>
<legend v-if="title">{{ title }}</legend>
<slot></slot>
</fieldset>
<div v-if="$slots.actions" class="form-actions" :class="{'pointer-events-none opacity-50': isSubmitting}">
<slot name="actions" :isSubmitting="isSubmitting"></slot>
</div>
</form>
</template>Parent:
<template>
<FormSection title="Shipping address" @submit="onSubmit">
<input v-model="form.address" placeholder="Address" />
<input v-model="form.city" placeholder="City" />
<template #actions>
<button type="button" @click="cancel">Cancel</button>
<button type="submit">Save</button>
</template>
</FormSection>
</template>5. Tabs with dynamic slot names
Tabs can be implemented with a slot per tab; the slot name is the tab id.
AppTabs.vue:
<script setup lang="ts">
import { ref, computed } from "vue";
const props = defineProps<{
tabs: Array<{ id: string; label: string }>;
}>();
const activeId = ref(props.tabs[0]?.id ?? "");
const activeSlot = computed(() => activeId.value);
</script>
<template>
<div class="tabs">
<div class="tabs-header">
<button
v-for="tab in tabs"
:key="tab.id"
:class="{ active: activeId === tab.id }"
@click="activeId = tab.id"
>
{{ tab.label }}
</button>
</div>
<div class="tabs-panels">
<template v-for="tab in tabs" :key="tab.id">
<div v-show="activeId === tab.id">
<slot :name="tab.id"></slot>
</div>
</template>
</div>
</div>
</template>Parent:
<template>
<AppTabs
:tabs="[
{ id: 'overview', label: 'Overview' },
{ id: 'settings', label: 'Settings' },
]"
>
<template #overview>
<p>Overview content...</p>
</template>
<template #settings>
<p>Settings form...</p>
</template>
</AppTabs>
</template>Here the slot names come from data (tab.id), which is a good fit for dynamic slot names.
(For an even smoother tab component design, checkout our article on Tightly Coupled Components Vue Components with Provide/Inject)
Frequently Asked Questions (FAQ)
What are slots in Vue?
Slots are placeholders a child component exposes so its parent component can inject custom HTML or template markup into it. The child defines the slot layout, while the parent supplies the rendered content.
What is the difference between default, named, and scoped slots?
- Default slot: A single unnamed slot (
<slot />) for passing general inner content. - Named slots: Multiple slots specified by name (
<slot name="header" />and<template #header>) to target distinct regions. - Scoped slots: Slots that pass data from the child component back to the parent (
<slot :item="item" />), allowing the parent to control row or item markup while using child data.
How do scoped slots work in Vue 3?
The child binds data props onto the <slot> tag (<slot :item="item" />). The parent receives those props on <template #default="{ item }"> and uses them directly within the slotted markup.
Can a Vue slot have a fallback?
Yes. Anything placed between the opening and closing <slot>Fallback Content</slot> tags inside the child acts as fallback content. It renders only when the parent does not pass any content for that slot.
How do I pass a dynamic slot name?
In the child, bind the slot name (<slot :name="dynamicName" />). In the parent, target it dynamically using bracket syntax (<template #[dynamicName]>).
Vue.js Slots Summary
- Default slot: One unnamed
<slot>; parent content between the component tags goes there. Use fallback content inside<slot>for defaults. - Named slots: Use
<slot name="...">and<template #name>(orv-slot:name) to target specific regions (header, footer, sidebar, etc.). - Scoped slots: Bind attributes on
<slot>; the parent receives them inv-slot="props"or#default="props"and can destructure. Use for lists, tables, and any case where the child owns the data but the parent owns the presentation. - Dynamic slot names: Use
#[dynamicName]orv-slot:[dynamicName]when the slot name is computed or from config (e.g. layouts, tabs). - Conditional regions: Use
v-if="$slots.name"so you only render wrapper elements when the parent actually provided that slot.
Once you're comfortable with these patterns, you can design components that stay generic and reusable while giving each screen full control over content and layout. For more structured learning, check out a course like Vue Component Design: Master Scalable Vue.js Patterns, which covers slot props and other component patterns in depth. Or you can dive deeper into components module in our flagship Vue.js 3 Master Class to build production-ready Vue 3 applications with industry-proven design patterns!
Slots are placeholders a component exposes so its parent can inject content. The child renders a slot; the parent supplies the markup where the component is used.
A default slot is a single unnamed hole. Named slots expose several holes you target by name. Scoped slots pass data from the child to the parent's slot content.
The child binds data on the slot tag (<slot :item="item" />), and the parent reads it with <template #default="{ item }">. It is how data tables let the parent control row rendering while the child owns the data.
Yes. Markup between the slot tags is fallback content, rendered only when the parent provides nothing.
Bind the name (<slot :name="dynamicName" />) and target it with <template #[dynamicName]>.
Start learning Vue.js for free

Comments
Latest Vue School Articles
5 Component Design Patterns to Boost Your Vue.js Applications

Vibe Coding a Collaborative Editor with Comment Support with Nuxt UI and Jazz

Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.
More than 200.000 users have already joined us. You are welcome too!
© All rights reserved. Made with ❤️ by BitterBrains, Inc.


