Fix: Vue Slot Not Working — Named Slots Not Rendering or Scoped Slot Data Not Accessible
Part of: React & Frontend Errors
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. The root cause of most “slot not working” reports is one of three things: a name mismatch between parent and child, slot props being referenced outside the scope where they exist, or a leftover Vue 2 pattern that the Vue 3 compiler silently ignores.
Named slots in Vue 3 are pure string matches. When a child declares <slot name="header">, the parent must produce a <template> with exactly the directive v-slot:header (or the #header shorthand). Anything else — content placed loose between the component tags, a template with a different name, a template with the directive on a non-<template> element — goes to the default slot. There is no error, no warning, just empty space where you expected your header to render. The same applies in reverse: a parent supplying #sidebar content to a child that does not declare <slot name="sidebar"> produces nothing.
Scoped slots add a second class of mistake. When the child writes <slot name="item" :item="item">, it is exposing item as a slot prop. The parent must destructure those props on the surrounding <template> tag using v-slot:item="{ item }" or #item="slotProps". Referencing item from outside that template scope — or from a parent template without v-slot at all — gives you undefined. The error is silent because Vue evaluates the expression in the parent’s render scope, where item may not exist, and Vue treats undefined references as empty strings by default.
The third source of confusion is the cross-version baggage. Vue 2 allowed slot="name" and slot-scope="props" on regular elements, and a lot of legacy tutorials still show those forms. Vue 3 requires v-slot on a <template> element or directly on a component. Putting v-slot on a <div> is a compile error. Code that “used to work” before a Vue 3 migration is the single most common cause of the slot-not-rendering bug; the compiler upgrade silently drops the directive, the slot falls through to the default, and the team spends an afternoon hunting for it.
Version History: Vue Slot API Evolution
Vue’s slot API has changed enough across the major and minor releases that knowing which version you target dictates which syntax compiles.
- Vue 2.0 (September 2016) — original slot API. Named slots used
slot="name"on the element itself. Scoped slots usedslot-scope="props". Both forms allowed on any element. - Vue 2.6 (February 2019) — introduced the unified
v-slotdirective and its#shorthand. The oldslotandslot-scopeattributes were soft-deprecated but still worked. This is the form most current Vue 2 codebases use. - Vue 3.0 (September 2020) — major breaking release. The legacy
slotandslot-scopeattributes were removed entirely. Scoped slots and named slots are now unified underv-slot, which can only appear on<template>elements or directly on a component. The old workarounds for fragment slots are no longer needed because Vue 3 supports multi-root templates. - Vue 3.2 (August 2021) —
<script setup>and the Composition API reached stable.useSlots()anddefineSlots<T>()(added in 3.3) became the canonical way to introspect slots from<script setup>. The Options API equivalent (this.$slots) is still supported. - Vue 3.3 (May 2023) — added the
defineSlots<T>()macro for typed slot declarations in<script setup lang="ts">, plus generic components via<script setup lang="ts" generic="T">. Slot prop types are now first-class in templates. - Vue 3.4 (December 2023) — performance improvements to slot resolution (~2x faster for components with many slots) and the
v-bindsame-name shorthand (:iteminstead of:item="item"). Slot props benefit from the same shorthand inside scoped slots. - Vue 3.5+ (2024) — stable reactivity props destructuring extends to slot props in
<script setup>. Combined withdefineSlots, this gives full type safety end to end.
If your codebase mixes Vue 2.6 syntax with Vue 3 components — which happens during long migrations — the slot directives will not be portable. Run a codemod (@vue/migration-helper) before assuming a slot bug is a logic problem.
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?.().
Mixing v-slot shorthand with directive arguments — #item and v-slot:item are equivalent, but you cannot abbreviate a dynamic slot as #[name] if there is also an argument bind on the same template. Vue’s compiler rejects this with a confusing “duplicate v-slot” error. Pick one form per template and stick with it across the file.
Functional components in Vue 3 don’t expose $slots the same way — Vue 3 functional components receive slots as the second argument ((props, { slots, attrs, emit }) => ...), not via this.$slots. If you migrated a Vue 2 functional component without updating the slot access, it returns undefined and nothing renders. Switch to the setup-style signature or convert it to a normal SFC.
Slot fallbacks evaluated even when content is provided — if your slot fallback calls an expensive function (<slot>{{ loadHeavyData() }}</slot>), Vue evaluates that expression once during compilation regardless of whether the parent provides content. Move the call into a computed property guarded by $slots.default (or useSlots().default) so it only runs when the fallback is actually rendered.
For related Vue issues, see Fix: Vue Composable Not Reactive, Fix: Vue Computed Not Updating, Fix: Vue Reactive Data Not Updating, and Fix: Vue Teleport Not Rendering.
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.