Skip to content

Fix: Vue Slot Not Working — Named Slots Not Rendering or Scoped Slot Data Not Accessible

FixDevs ·

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 the default slot.
  • Scoped slot data not destructured — when a child passes data via <slot :item="item">, the parent must use v-slot:default="slotProps" or #default="{ item }" to access it. Referencing item directly doesn’t work.
  • v-slot only on <template> or components — unlike Vue 2’s slot attribute, Vue 3’s v-slot can 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 default slot. 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles