frontend / vue / 08_slots_and_scoped_slots.md

Slots and Scoped Slots

5 min read source

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.)
  • $slots object at runtime — Vue 3 provides useSlots() in setup or this.$slots in 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.foo is truthy. Test with useSlots().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

Further reading