Frontend / Vue / 08_slots_and_scoped_slots.md

Slots and Scoped Slots

Updated 5 interview angles 5 min read source
On this page13
  1. Default slot — show me.
  2. Fallback content?
  3. Named slots.
  4. Scoped slots — what problem do they solve?
  5. Real-world scoped slot example — a combobox.
  6. Typing slots in TS.
  7. What’s v-slot shorthand?
  8. Dynamic slot names.
  9. How does this compare to React’s children?
  10. Gotchas
  11. Related
  12. Further reading
  13. Interview angle

Slots and Scoped Slots

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.

Default slot — show me.

vue
<!-- 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.

Fallback content?

vue
<template>
  <button>
    <slot>Submit</slot>      <!-- "Submit" if no slot content provided -->
  </button>
</template>

If the parent supplies nothing between <Button></Button>, the fallback renders.

Named slots.

vue
<!-- 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.

Scoped slots — what problem do they solve?

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.

vue
<!-- 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:

tsx
// React equivalent
<List items={users} renderItem={(item, index) => (
  <>
    <strong>{index}: {item.name}</strong>
    <em>{item.email}</em>
  </>
)} />

Real-world scoped slot example — a combobox.

vue
<!-- 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.

Typing slots in TS.

Vue 3.3+ supports typed slots via defineSlots:

vue
<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.

What’s v-slot shorthand?

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.

Dynamic slot names.

vue
<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.

How does this compare to React’s children?

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

  • 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 }".

Further reading

Interview angle 5

  • “What are slots, in React terms?” - the default slot is children, named slots are multiple children props, and scoped slots are render props. Same three patterns, different syntax, and saying so is usually the fastest way to show you understand both.
  • “What are scoped slots actually for?” - letting a component own the behaviour - state, keyboard handling, ARIA wiring - while the consumer owns the rendering. That is exactly the headless-component split that Radix and Headless UI do in React, and Vue has had the primitive for it since Vue 2.
  • “Whose scope does slot content compile in?” - the parent’s. Slot content sees the parent’s data and not the child’s, which is why a scoped slot has to explicitly pass values back out. People expect the opposite and it is the main source of confusion here.
  • “How do you render conditionally based on what was passed?” - useSlots(), checking whether the named slot exists. It is how you avoid rendering an empty wrapper element around a slot nobody filled.
  • “Can slots be typed?” - yes, defineSlots<T>() from Vue 3.3. Worth doing for any component other people consume, since it is the only way the slot’s props show up in a consumer’s editor.