Fix: Vue Slot Not Working — Named Slots Not Rendering or Scoped Slot Data Not Accessible
Quick Answer
How to fix Vue 3 slot issues — v-slot syntax, named slots, scoped slots passing data, default slot content, fallback content, and dynamic slot names.
The Problem
A named slot doesn’t render its content:
<!-- Parent -->
<MyCard>
<template #header>Card Title</template> <!-- Never shows -->
<p>Card body content</p>
</MyCard>
<!-- MyCard.vue — slot missing the name attribute -->
<template>
<div class="card">
<div class="card-header">
<slot></slot> <!-- Default slot — doesn't receive #header content -->
</div>
<div class="card-body">
<slot name="body"></slot> <!-- Named 'body', not 'default' -->
</div>
</div>
</template>Or scoped slot data isn’t accessible in the parent:
<!-- Parent — trying to access item from the slot -->
<DataList>
<template #item>
<div>{{ item.name }}</div> <!-- 'item' is undefined -->
</template>
</DataList>Or default slot fallback content shows even when content is provided:
<MyComponent>
<p>My content</p>
</MyComponent>
<!-- Fallback text still visible -->Why This Happens
Vue 3 changed the slot API from Vue 2, and several patterns are easy to confuse:
- Named slot mismatch —
<slot name="header">in the child requires<template v-slot:header>(or#header) in the parent. Content without a named template goes to thedefaultslot. - Scoped slot data not destructured — when a child passes data via
<slot :item="item">, the parent must usev-slot:default="slotProps"or#default="{ item }"to access it. Referencingitemdirectly doesn’t work. v-slotonly on<template>or components — unlike Vue 2’sslotattribute, Vue 3’sv-slotcan only be used on<template>elements (for named/scoped slots) or directly on a component.- Default slot content vs fallback — content placed between component tags goes into the
defaultslot. If the child’s<slot>has inner content, that’s the fallback shown only when no slot content is provided.
Fix 1: Match Named Slot Names Exactly
Slot names in child and parent must match exactly:
<!-- Child: MyCard.vue -->
<template>
<div class="card">
<!-- Named slot 'header' -->
<div class="card-header">
<slot name="header">
Default Header <!-- Fallback if no #header provided -->
</slot>
</div>
<!-- Default slot (unnamed) -->
<div class="card-body">
<slot></slot>
</div>
<!-- Named slot 'footer' -->
<div class="card-footer">
<slot name="footer"></slot>
</div>
</div>
</template><!-- Parent -->
<template>
<MyCard>
<!-- v-slot:header or shorthand #header -->
<template #header>
<h2>My Card Title</h2>
</template>
<!-- Default slot — no template needed for simple content -->
<p>This goes into the default slot.</p>
<p>Multiple elements are fine.</p>
<!-- Named footer slot -->
<template #footer>
<button>Close</button>
</template>
</MyCard>
</template>Shorthand syntax reference:
<!-- Full syntax -->
<template v-slot:header>...</template>
<!-- Shorthand (preferred) -->
<template #header>...</template>
<!-- Default slot shorthand -->
<template #default>...</template>
<!-- Or just place content directly inside the component -->Fix 2: Access Scoped Slot Data
Scoped slots let the child pass data up to the parent’s slot content:
<!-- Child: DataList.vue -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<!-- Pass item to the parent's slot -->
<slot name="item" :item="item" :index="index"></slot>
</li>
</ul>
</template>
<script setup>
defineProps<{ items: Array<{ id: number; name: string }> }>();
</script><!-- Parent — access scoped slot data -->
<template>
<DataList :items="users">
<!-- WRONG — item is not in scope here -->
<template #item>
<div>{{ item.name }}</div> <!-- item is undefined -->
</template>
<!-- CORRECT — destructure slot props -->
<template #item="{ item, index }">
<div>{{ index + 1 }}. {{ item.name }}</div>
</template>
<!-- OR use the full slotProps object -->
<template #item="slotProps">
<div>{{ slotProps.item.name }}</div>
</template>
</DataList>
</template>Scoped slots with TypeScript:
<!-- Child — define slot types -->
<script setup lang="ts">
interface Item {
id: number;
name: string;
active: boolean;
}
defineProps<{ items: Item[] }>();
// Define slot types for TypeScript support
defineSlots<{
item(props: { item: Item; index: number }): any;
empty(props: {}): any;
}>();
</script>
<template>
<div>
<template v-if="items.length > 0">
<div v-for="(item, index) in items" :key="item.id">
<slot name="item" :item="item" :index="index" />
</div>
</template>
<template v-else>
<slot name="empty" />
</template>
</div>
</template><!-- Parent — TypeScript knows the slot prop types -->
<DataList :items="users">
<template #item="{ item, index }">
<!-- item is typed as Item, index as number -->
<span :class="{ 'opacity-50': !item.active }">
{{ index + 1 }}. {{ item.name }}
</span>
</template>
<template #empty>
<p>No users found.</p>
</template>
</DataList>Fix 3: Default Slot with Fallback Content
Provide fallback content shown only when no slot content is given:
<!-- Child: Button.vue -->
<template>
<button class="btn">
<slot>
<!-- Fallback — shown if parent provides no content -->
Click me
</slot>
</button>
</template><!-- Parent examples -->
<!-- Uses fallback — renders "Click me" -->
<Button />
<Button></Button>
<!-- Overrides fallback — renders "Submit" -->
<Button>Submit</Button>
<!-- Multi-element override -->
<Button>
<span class="icon">✓</span>
Save changes
</Button>Check if slot has content from inside the component:
<script setup>
import { useSlots } from 'vue';
const slots = useSlots();
const hasHeader = computed(() => !!slots.header);
const hasFooter = computed(() => !!slots.footer);
</script>
<template>
<div class="card">
<!-- Only render header wrapper if content is provided -->
<header v-if="hasHeader" class="card-header">
<slot name="header" />
</header>
<div class="card-body">
<slot />
</div>
<footer v-if="hasFooter" class="card-footer">
<slot name="footer" />
</footer>
</div>
</template>Fix 4: Dynamic Slot Names
Use dynamic slot names when the slot to target isn’t known at compile time:
<!-- Child: TabPanel.vue -->
<template>
<div>
<div v-for="tab in tabs" :key="tab.id" v-show="activeTab === tab.id">
<!-- Dynamic slot name based on tab id -->
<slot :name="tab.id">
<p>No content for {{ tab.label }}</p>
</slot>
</div>
</div>
</template><!-- Parent -->
<template>
<TabPanel :tabs="tabs" v-model:activeTab="currentTab">
<!-- Static named slots -->
<template #overview>
<OverviewPanel />
</template>
<template #settings>
<SettingsPanel />
</template>
<!-- Dynamic slot name -->
<template v-for="tab in customTabs" :key="tab.id" #[tab.id]>
<component :is="tab.component" />
</template>
</TabPanel>
</template>Fix 5: Renderless Components with Scoped Slots
Scoped slots enable powerful renderless component patterns — logic in the child, rendering in the parent:
<!-- Child: FetchData.vue — handles fetching, parent handles rendering -->
<script setup lang="ts">
interface Props {
url: string;
}
const props = defineProps<Props>();
const data = ref(null);
const loading = ref(true);
const error = ref<string | null>(null);
onMounted(async () => {
try {
const res = await fetch(props.url);
data.value = await res.json();
} catch (e) {
error.value = e.message;
} finally {
loading.value = false;
}
});
</script>
<template>
<slot :data="data" :loading="loading" :error="error" />
</template><!-- Parent — full control over rendering -->
<FetchData url="/api/users">
<template #default="{ data, loading, error }">
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<ul v-else>
<li v-for="user in data" :key="user.id">{{ user.name }}</li>
</ul>
</template>
</FetchData>Fix 6: Slots in Vue Router Views
Using slots with <RouterView> and layouts:
<!-- App.vue — pass slot content through router views -->
<template>
<RouterView v-slot="{ Component }">
<transition name="fade">
<component :is="Component" />
</transition>
</RouterView>
</template><!-- Layout.vue — named slots for page structure -->
<template>
<div class="layout">
<header>
<slot name="header">
<DefaultHeader />
</slot>
</header>
<main>
<slot />
</main>
<aside v-if="$slots.sidebar">
<slot name="sidebar" />
</aside>
</div>
</template><!-- A page component using the layout -->
<template>
<Layout>
<template #header>
<PageHeader title="Dashboard" />
</template>
<template #sidebar>
<DashboardNav />
</template>
<!-- Default slot — main content -->
<DashboardContent />
</Layout>
</template>Still Not Working?
v-slot on non-template elements in Vue 3 — in Vue 2, you could use slot="name" on any element. In Vue 3, v-slot (or #name) must be used on a <template> or directly on a component. Using it on a <div> causes a compile error.
Slot content not reactive — slot content is evaluated in the parent’s scope. If you pass a reactive value from the parent, it updates as expected. If the child mutates a prop and expects the slot to reflect that, use a scoped slot to pass the value back up.
Multiple default slots — a component can only have one <slot> without a name (the default slot). Placing content directly in the component goes to the default slot; there’s no way to have two unnamed slots. Use named slots for separate content areas.
$slots.default in the Options API — to check if the default slot has content in the Options API, use this.$slots.default. It returns an array of VNodes or undefined if empty. In the Composition API, use useSlots().default?.().
For related Vue issues, see Fix: Vue Composable Not Reactive and Fix: Vue Computed Not Updating.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Pinia State Not Reactive — Store Changes Not Updating the Component
How to fix Pinia store state not updating components — storeToRefs for destructuring, $patch for partial updates, avoiding reactive() wrapping, getters vs computed, and SSR hydration.
Fix: Vue Computed Property Not Updating — Reactivity Not Triggered
How to fix Vue computed properties not updating — reactive dependency tracking, accessing nested objects, computed setters, watchEffect vs computed, and Vue 3 reactivity pitfalls.
Fix: Vue v-model Not Working on Custom Components — Prop Not Syncing
How to fix Vue v-model on custom components — defineModel, modelValue/update:modelValue pattern, multiple v-model bindings, v-model modifiers, and Vue 2 vs Vue 3 differences.
Fix: Vue Router Navigation Guard Not Working — beforeEach and Route Guards
How to fix Vue Router navigation guards not working — beforeEach, beforeEnter, in-component guards, async guards, redirect loops, and route meta authentication patterns.