What are slots in Vue?
Slots let a parent component inject custom content into specific placeholders defined in a child component's template.
Theory
TL;DR
- Think of slots like USB ports: the child defines the ports, the parent plugs in whatever content it wants
- Three types: default (unnamed), named (targeted by name), scoped (child passes data back to parent's content)
- Slots pass content (HTML, components); props pass data (strings, objects)
- Decision rule: parent needs to control the UI? Use a slot. Passing data only? Use props.
- Vue 3 syntax:
#nameorv-slot:name(the oldslot="name"attribute is deprecated)
Quick example
<!-- Alert.vue -->
<template>
<div class="alert">
<slot name="icon">⚠️</slot> <!-- named slot with fallback -->
<slot>Default message</slot> <!-- default slot -->
</div>
</template><!-- Parent -->
<Alert>
<template #icon>🚨</template>
<p>Custom urgent message!</p>
</Alert>
<!-- Renders: 🚨 Custom urgent message! -->If the parent leaves out #icon, the child's fallback ⚠️ renders instead. That fallback content inside <slot> is what shows when nothing is provided.
Slots vs props
Props carry data: strings, numbers, objects. Slots carry structure: HTML, other components, anything you write in a template. You cannot pass a <CustomButton> or a styled paragraph through a prop without it turning into an escaped string. Slots solve exactly that problem.
Slot content also lives in the parent's scope. The parent writes it, so it has access to parent variables and methods. Scoped slots add a reverse channel where the child shares its own data back up.
Default, named, and scoped
Default slot is the unnamed catch-all. Any content placed directly inside a component tag fills it.
Named slots let you target multiple placeholders independently. A layout component can have header, default, and footer slots, each controlled separately.
<!-- Layout.vue -->
<template>
<header><slot name="header" /></header>
<main><slot /></main>
<footer><slot name="footer" /></footer>
</template><!-- Usage -->
<Layout>
<template #header><h1>Page Title</h1></template>
<p>Main content here</p>
<template #footer><p>© 2025</p></template>
</Layout>Scoped slots are the most flexible. The child exposes data through the slot, and the parent decides how to render it. Classic use case: a reusable list that owns the data but delegates rendering to the parent.
<!-- UserList.vue -->
<template>
<ul>
<li v-for="user in users" :key="user.id">
<slot :item="user" />
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue'
const users = ref([{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }])
</script><!-- Dashboard.vue -->
<UserList>
<template #default="{ item }">
<strong>{{ item.name }}</strong>
<button @click="editUser(item)">Edit</button>
</template>
</UserList>The list handles iteration and data. The parent controls presentation. In most dashboard projects I have worked on, this exact pattern shows up in every data table. It is also why component libraries like Vuetify and Element Plus are so configurable without requiring you to fork them.
When to use
- Generic container (card, modal, panel) → default slot
- Multiple distinct areas (header, body, footer) → named slots
- Child owns data, parent controls rendering (tables, lists, dropdowns) → scoped slots
- Passing only strings or primitives → props are enough
- Deep data sharing without prop drilling → provide/inject
How Vue handles slots internally
Vue's template compiler scans <slot> tags and creates slot anchors in the child's render function. At runtime, parent content compiles into a separate render function that gets injected at those anchors. Scoped slots work the same way, except the child passes a props object when calling the slot function. In Vue 3, reactive slot data flows through Proxy, so changes in child data automatically update rendered slot content without re-rendering the whole tree.
You can check whether a slot was provided using $slots.name in templates, or useSlots() in <script setup>:
<script setup>
import { useSlots } from 'vue'
const slots = useSlots()
</script>
<template>
<slot name="tab2" />
<p v-if="!slots.tab2">No content provided</p>
</template>Common mistakes
Passing HTML through a prop:
<!-- Wrong -->
<Child :content="'<p>Hello</p>'" />
<!-- Renders as an escaped string, not HTML --><!-- Correct -->
<Child><p>Hello</p></Child>Props serialize values. HTML passed as a string renders as <p>Hello</p>.
Using deprecated Vue 2 slot syntax in Vue 3:
<!-- Wrong in Vue 3 -->
<Child slot="header">Content</Child><!-- Correct -->
<Child v-slot:header>Content</Child>
<!-- shorthand -->
<Child #header>Content</Child>The slot attribute is deprecated. Vue 3's compiler expects v-slot:name or #name.
Trying to mutate scoped slot props:
<!-- Wrong: mutation does not persist -->
<List v-slot="{ item }">
{{ item.name = 'Changed' }}
</List>Slot props are read-only in the parent template. They arrive through a Proxy that ignores mutations. To update child data, emit an event and handle it in the parent:
<List @update="handleUpdate" v-slot="{ item }">
<button @click="$emit('update', item)">Update</button>
</List>Unnamed nested slots colliding:
When you nest components without naming inner slots, multiple <slot /> targets fight over the same default. Always name slots when a component has more than one content area.
Real-world usage
- Vuetify:
<v-card><template #text>Custom content</template></v-card> - Element Plus:
<el-table><template #default="{ row }">...</template></el-table> - Nuxt UI: layout components expose
<slot name="hero" />for page sections - PrimeVue:
<DataTable><template #body="slotProps">...</template></DataTable>
Scoped slots are the foundation of data-display components across every major Vue UI library.
Follow-up questions
Q: What is the difference between a default slot and a named slot?
A: Default fills the unnamed <slot />. Named slots target a specific <slot name="x" /> via #x or v-slot:x. One component can have both at the same time.
Q: Can slot content access the child component's data?
A: Not directly. Slot content lives in the parent's scope and can only see parent variables. Scoped slots bridge that gap: <slot :item="user" /> exposes user to the parent via v-slot="{ item }".
Q: How do you detect whether a slot was filled by the parent?
A: In templates, check $slots.name. In <script setup>, call useSlots() from the Composition API and inspect the returned object reactively.
Q: What changed between Vue 2 and Vue 3 slot syntax?
A: Vue 2 used slot="name" on elements and slot-scope="data" for scoped slots. Vue 3 replaced both with v-slot:name="data" (shorthand #name="data"). The old syntax still exists but is deprecated and may be removed.
Q: How does Vue optimize scoped slots when used with Suspense or async components?
A: In Vue 3, slot content can suspend independently from the parent. A child can wrap a slot in <Suspense> to show a fallback while async content inside the slot resolves. Reactive slot data propagates through Proxy, so only the affected slot re-renders when child data changes, not the entire component tree.
Examples
Basic: Card component with named slots and fallbacks
<!-- Card.vue -->
<template>
<div class="card">
<div class="card-header">
<slot name="header">Default Title</slot>
</div>
<div class="card-body">
<slot />
</div>
<!-- footer div only exists in DOM when parent provides content -->
<div class="card-footer" v-if="$slots.footer">
<slot name="footer" />
</div>
</div>
</template><!-- ProductPage.vue -->
<Card>
<template #header>
<h2>MacBook Pro</h2>
</template>
<p>Best laptop for developers.</p>
<p>Price: $2,499</p>
<template #footer>
<button @click="addToCart">Add to cart</button>
</template>
</Card>The v-if="$slots.footer" check removes the entire div from the DOM when the parent does not provide footer content. Not an empty block, a completely absent element. This matters for CSS layouts that target direct children.
Intermediate: Reusable data table with scoped slots
A common dashboard pattern: the table owns structure and iteration, each column delegates rendering to the parent.
<!-- DataTable.vue -->
<template>
<table>
<thead>
<tr>
<th v-for="col in columns" :key="col.key">{{ col.label }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in rows" :key="row.id">
<td v-for="col in columns" :key="col.key">
<!-- scoped slot exposes both the full row and the cell value -->
<slot :name="col.key" :row="row" :value="row[col.key]">
{{ row[col.key] }} <!-- fallback: plain text -->
</slot>
</td>
</tr>
</tbody>
</table>
</template>
<script setup>
defineProps({ columns: Array, rows: Array })
</script><!-- UsersDashboard.vue -->
<DataTable :columns="cols" :rows="users">
<template #status="{ value }">
<span :class="value === 'active' ? 'badge-green' : 'badge-red'">
{{ value }}
</span>
</template>
<template #actions="{ row }">
<button @click="editUser(row)">Edit</button>
<button @click="deleteUser(row.id)">Delete</button>
</template>
</DataTable>Columns without a matching slot fall back to plain text automatically. This is the same architecture <el-table> in Element Plus uses.
Senior: Reactive slot detection with useSlots
<!-- DashboardWidget.vue -->
<template>
<div class="widget">
<div class="widget-header">
<slot name="title">
<span>{{ defaultTitle }}</span>
</slot>
<!-- actions bar only mounts when the parent fills it -->
<div v-if="slots.actions" class="widget-actions">
<slot name="actions" />
</div>
</div>
<div class="widget-body">
<slot />
</div>
</div>
</template>
<script setup>
import { useSlots } from 'vue'
defineProps({ defaultTitle: { type: String, default: 'Widget' } })
const slots = useSlots()
</script><!-- AdminPanel.vue -->
<DashboardWidget default-title="Traffic">
<template #actions>
<button @click="refresh">Refresh</button>
<button @click="exportData">Export</button>
</template>
<TrafficChart :data="chartData" />
</DashboardWidget>useSlots() returns a reactive object. If the parent adds or removes #actions dynamically, the widget reacts without any extra logic. The approach keeps empty wrapper divs out of the rendered HTML, which matters when CSS grid or flex rules target direct children.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.