Slots

This section documents the current Vue 3.x release line as published at the official Vue.js documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. The Composition API with <script setup> is the authoring style used throughout; the Options API is shown only as a contrast.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vue and its ecosystem iterate quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

Props pass data into a component; slots pass template content. The parent writes markup between the component’s tags and the child decides where — and whether — to render it. Scoped slots add a return channel, letting the child hand data back out to that markup.

A parent template’s fragments projected into a child component’s named slot outlets, with a scoped slot passing a row object back up to the parent’s slot template

The default slot and fallback content

<slot> in the child marks where the parent’s content goes:

<!-- FancyButton.vue -->
<template>
  <button class="fancy-btn">
    <slot>Click me</slot>   <!-- fallback: shown only when the parent passes nothing -->
  </button>
</template>
<!-- Parent -->
<FancyButton>Save changes</FancyButton>
<FancyButton />               <!-- renders "Click me" -->

Slot content compiles in the parent’s scope: it can read the parent’s state but not the child’s. See Slots.

Named slots

A component can expose several outlets. Give each <slot> a name (the unnamed one is implicitly default) and target it from the parent with <template #name>:

<!-- BaseLayout.vue -->
<template>
  <div class="layout">
    <header><slot name="header" /></header>
    <main><slot /></main>
    <footer><slot name="footer" /></footer>
  </div>
</template>
<!-- Parent -->
<BaseLayout>
  <template #header><h1>Page title</h1></template>

  <p>Main body content goes in the default slot.</p>

  <template #footer><small>Footer text</small></template>
</BaseLayout>

#header is shorthand for v-slot:header. See Named Slots.

Conditional slots

$slots is an object keyed by the names of the slots the parent actually provided. Use it to drop wrapper markup when a slot is empty:

<template>
  <div class="card">
    <header v-if="$slots.header" class="card-header">
      <slot name="header" />
    </header>
    <div class="card-body"><slot /></div>
  </div>
</template>

Dynamic slot names

A name in v-slot can be an expression in square brackets:

<template>
  <BaseLayout>
    <template #[dynamicSlot]>Goes wherever dynamicSlot names</template>
  </BaseLayout>
</template>

Scoped slots

To let slot content use data only the child has, the child passes it as attributes on <slot>, and the parent receives a slot props object:

<!-- TodoList.vue -->
<script setup>
defineProps({ items: Array })
</script>

<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot :item="item" :index="item.id">{{ item.text }}</slot>
    </li>
  </ul>
</template>
<!-- Parent: destructure the slot props -->
<TodoList :items="todos">
  <template #default="{ item, index }">
    <strong>{{ index }}.</strong> {{ item.text }}
  </template>
</TodoList>

When only the default slot is scoped, v-slot can go straight on the component tag — <TodoList v-slot="\{ item }">. See Scoped Slots.

Named and scoped together

Every named slot can carry its own slot props; each <template> destructures the object for that slot:

<DataTable :rows="rows">
  <template #row="{ row }">
    <td>{{ row.name }}</td>
    <td>{{ row.email }}</td>
  </template>

  <template #empty>
    <td colspan="2">No results</td>
  </template>
</DataTable>

See Scoped Slots for the mixed case.

The renderless component pattern

A component with no markup of its own — its template is a single scoped <slot> passing computed values out — packages behaviour while leaving all rendering to the parent:

<!-- MouseTracker.vue -->
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const x = ref(0)
const y = ref(0)
const update = (e) => { x.value = e.pageX; y.value = e.pageY }

onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>

<template>
  <slot :x="x" :y="y" />
</template>
<MouseTracker v-slot="{ x, y }">
  <p>Pointer at {{ x }}, {{ y }}</p>
</MouseTracker>

The Vue docs note that a composable usually does this job with less overhead; the renderless pattern earns its keep when the logic is inseparable from a slot-shaped API. See Renderless Components.

Typing slots

defineSlots() is a <script setup> macro that emits no runtime code and only attaches types: each key is a slot name and its function type describes the slot props the child will pass.

defineSlots<{
  default(props: { item: Todo; index: number }): any
  empty(): any
}>()

See also