Slots and Scoped Slots
TL;DR
Slots are Vue’s primary composition primitive — the equivalent of React’s children. A default slot renders whatever the parent passes between component tags. Named slots are like multiple children (header/body/footer). Scoped slots are slots that receive data from the child — the equivalent of React’s “render props” — and they’re how reusable components like data tables, lists, and combo boxes expose their internals without surrendering control.
Interview Q&A
Q: Default slot — show me.
A:
<!-- Card.vue -->
<template>
<div class="card">
<slot /> <!-- the default slot -->
</div>
</template>
<!-- Parent -->
<Card>
<h2>Hello</h2>
<p>This goes in the slot.</p>
</Card>
<slot> is the insertion point. The parent’s content lands wherever the slot lives.
Q: Fallback content?
A:
<template>
<button>
<slot>Submit</slot> <!-- "Submit" if no slot content provided -->
</button>
</template>
If the parent supplies nothing between <Button></Button>, the fallback renders.
Q: Named slots.
A:
<!-- Layout.vue -->
<template>
<div>
<header><slot name="header" /></header>
<main><slot /></main> <!-- default slot -->
<footer><slot name="footer" /></footer>
</div>
</template>
<!-- Parent -->
<Layout>
<template #header>
<h1>Title</h1>
</template>
<p>Main content here goes to the default slot.</p>
<template #footer>
<p>© 2026</p>
</template>
</Layout>
#header is shorthand for v-slot:header. The default slot can be <template #default> or just unwrapped content.
Q: Scoped slots — what problem do they solve?
A: A reusable component (a <List>, <DataTable>, <Combobox>) needs to render items without knowing their shape. The parent should control the rendering, but the child controls the iteration. Scoped slots: the child passes data out through the slot.
<!-- List.vue -->
<script setup>
defineProps<{ items: any[] }>();
</script>
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot name="item" :item="item" :index="$index" />
</li>
</ul>
</template>
<!-- Parent -->
<List :items="users">
<template #item="{ item, index }">
<strong>{{ index }}: {{ item.name }}</strong>
<em>{{ item.email }}</em>
</template>
</List>
The child passes item and index through the slot props; the parent destructures and renders. This is structurally identical to React’s render props:
// React equivalent
<List items={users} renderItem={(item, index) => (
<>
<strong>{index}: {item.name}</strong>
<em>{item.email}</em>
</>
)} />
Q: Real-world scoped slot example — a combobox.
A:
<!-- Combobox.vue -->
<script setup lang="ts">
defineProps<{ items: T[]; selected: T | null }>();
const emit = defineEmits<{ "update:selected": [item: T] }>();
// internal state — open/closed, highlight, etc.
const open = ref(false);
const highlightIndex = ref(0);
</script>
<template>
<div>
<slot name="trigger" :selected="selected" :open="open" :toggle="() => open = !open" />
<ul v-if="open">
<li v-for="(item, i) in items" :key="item.id" :class="{ hl: i === highlightIndex }">
<slot name="item" :item="item" :index="i" :highlight="i === highlightIndex" />
</li>
</ul>
</div>
</template>
<!-- Parent — fully customizes rendering -->
<Combobox :items="users" v-model:selected="picked">
<template #trigger="{ selected, toggle }">
<button @click="toggle">{{ selected?.name ?? "Choose..." }}</button>
</template>
<template #item="{ item, highlight }">
<Avatar :user="item" />
<span :class="{ bold: highlight }">{{ item.name }}</span>
</template>
</Combobox>
The component owns behavior (open/close, keyboard, ARIA); the parent owns rendering. This is exactly how Headless UI / Radix work in React, expressed with Vue’s slot system.
Q: Typing slots in TS.
A: Vue 3.3+ supports typed slots via defineSlots:
<script setup lang="ts">
defineProps<{ items: User[] }>();
defineSlots<{
item(props: { item: User; index: number; highlight: boolean }): any;
empty(): any;
}>();
</script>
This gives autocomplete + type checking on slot usages in the parent.
Q: What’s v-slot shorthand?
A:
| Long form | Shorthand |
|---|---|
v-slot:default |
(none — default slot doesn’t need a wrapper) |
v-slot:header |
#header |
v-slot:item="{ item, index }" |
#item="{ item, index }" |
Use shorthand in real code; the long form is the spec.
Q: Dynamic slot names.
A:
<MyComponent>
<template v-for="key in slotNames" #[key]="props">
{{ key }} content: {{ props.value }}
</template>
</MyComponent>
The square bracket is the dynamic-argument syntax. Less common, useful for table column rendering patterns where columns are data-driven.
Q: How does this compare to React’s children?
A:
| Concern | Vue | React |
|---|---|---|
| Single insertion point | default slot | children |
| Multiple insertion points | named slots | multiple props (header, footer, etc.) |
| Pass data out | scoped slot | render prop / children as function |
| Conditional rendering | <slot v-if=""> |
{condition && children} |
| Default content | <slot>fallback</slot> |
children ?? "fallback" |
Vue’s slot system is more named; React’s is more functional. Both achieve the same compositions.
Gotchas / edge cases
- Slot content compiles in the parent’s scope — variables/refs in the slot template refer to the parent’s data, not the child’s. (That’s why scoped slots exist — to expose child data outward.)
$slotsobject at runtime — Vue 3 providesuseSlots()in setup orthis.$slotsin Options API to programmatically check which slots were provided. Useful for conditional fallback rendering.- Empty slot ≠ no slot. A parent that provides an empty
<template #foo />does fill the slot —$slots.foois truthy. Test withuseSlots().foo?.()carefully. - Slot fallback runs every render of the child — if the fallback is expensive, optimize or hoist.
- Multiple roots in a slot — works fine; the slot just emits the fragment.
- Naming collision with prop names in scoped slot destructuring — you can rename:
#item="{ item: row, index }".
What a senior is expected to say
- “Slots are Vue’s composition primitive. Default slot = React
children; named slots = multiple children props; scoped slots = render props — same patterns, different syntax.” - “Scoped slots let a component own behavior (state, keyboard handling, ARIA) while the consumer owns rendering. This is the same separation Headless UI / Radix do in React.”
- “TS-typed slots via
defineSlots<>()are 3.3+; use them for any library-style component.” - “Slot content compiles in the parent’s scope — it sees the parent’s data, not the child’s. Scoped slots are how the child explicitly exposes its internals.”
- “Use
useSlots()to conditionally render based on what slots were provided.”
Cross-references
- Props/emits/v-model (the data side): 07_props_emits_vmodel.md
- Composition API patterns: 05_composition_vs_options.md
- Component library design (composition>configuration): ../14_frontend_system_design/09_design_a_component_library.md
Further reading
- Vue docs — Slots: https://vuejs.org/guide/components/slots.html
- Vue docs —
defineSlots(): https://vuejs.org/api/sft-script-setup.html#defineslots