Home / Blog / Vue Slots: The Complete Guide (Default, Named, Scoped)
Vue Slots: The Complete Guide (Default, Named, Scoped)

Vue Slots: The Complete Guide (Default, Named, Scoped)

Slots are how a Vue component accepts and renders content from its parent. The child defines a slot placeholder; the parent fills it at the point of use. Vue has three kinds: default slots for a single content hole, named slots for multiple holes, and scoped slots for passing data from the child back up to the parent's slot content.

Slots are one of Vue's most powerful features for building flexible, reusable components. They let the parent decide what to render while the child controls where and how it appears. Whether you're wrapping content in a card, building a layout, or customizing each row of a list, vue slots are the right tool. This guide walks you through the basics, advanced techniques, and plenty of practical scenarios you'll use every day.

Slot Type Comparison

Use this quick-reference matrix to understand when and how to use each slot type in Vue 3:

Slot type What it does Use when Syntax
Default One unnamed content hole Component wraps a single content block <slot />
Named Multiple labelled holes Layouts, cards (header/body/footer) <slot name="header" /><template #header>
Scoped Child exposes data to parent's content Lists, tables, renderless components <slot :item="item" /><template #default="{ item }">
Dynamic Name Slot name resolved at runtime Slot chosen dynamically by a variable <slot :name="dynamicName" /><template #[dynamicName]>

Vue.js Slots Basics

What Are Vue.js Slots?

A slot is a placeholder in a child component that gets filled with content passed from the parent. The child defines one or more <slot> outlets; the parent provides the content. That keeps the child's structure and behavior (e.g. styling, logic) while giving the parent full control over the actual HTML content.

Think of a button: the child might handle type, disabled state, and click handling, but the label (maybe along with an icon) comes from the parent. Slots are how you pass that label (or any template fragment) in.

Default Slot for Vue.js Slots

The simplest form is a single, unnamed slot—the default slot. Anything you put between the child's opening and closing tags is passed into that slot.

Child component (ButtonSubmit.vue):

<script setup lang="ts">
  // Handles submit logic, styling, etc.
</script>

<template>
  <button type="submit" class="btn btn-primary">
    <slot></slot>
  </button>
</template>

Parent usage:

<template>
  <ButtonSubmit><IconSave />Save changes</ButtonSubmit>
</template>

The text "Save changes" and the save icon is slotted into the button. You can pass any valid template elements, components, etc to the slot.

Fallback Content for Vue.js Slots

You can put content inside <slot> in the child. That content is used only when the parent does not provide anything for the slot. In other words, it's the slot's default.

<!-- ButtonSubmit.vue -->
<template>
  <button type="submit" class="btn btn-primary">
    <slot>Submit</slot>
  </button>
</template>

If the parent uses <ButtonSubmit></ButtonSubmit> with no content, "Submit" is shown. If the parent uses <ButtonSubmit>Save</ButtonSubmit>, "Save" is shown.

Slots and Template Props Patterns

Defining slot defaults with prop values is a common Vue component design pattern to progressively enhance a component's API. This makes for quick usage with the prop but maintains flexibility for more complex needs. For example, you could define a simple button component with a label prop along with a slot for a label:

<!-- Button.vue -->
<script setup lang="ts">
  defineProps<{ label?: string }>();
</script>

<template>
  <button type="submit" class="btn btn-primary">
    <slot>{{ label }}</slot>
  </button>
</template>

Named slots

When a component needs multiple distinct content areas, use named slots. The child gives each <slot> a name; the parent targets them with v-slot (or the # shorthand) on <template>.

Child component (Card.vue):

<script setup lang="ts">
  defineProps<{ title?: string }>();
</script>

<template>
  <article class="card">
    <header class="card-header">
      <slot name="header"></slot>
    </header>
    <div class="card-body">
      <slot></slot>
    </div>
    <footer class="card-footer">
      <slot name="footer"></slot>
    </footer>
  </article>
</template>

The unnamed <slot> is the default slot. The parent fills each slot by name:

<template>
  <Card>
    <template #header>
      <h2>Account settings</h2>
    </template>

    <template #default>
      <p>Update your email and password.</p>
    </template>

    <template #footer>
      <button>Save</button>
    </template>
  </Card>
</template>
  • #header is shorthand for v-slot:header.
  • #default targets the default slot. You can also put the main content directly between <Card> and </Card> without a wrapper; that goes to the default slot.

So: one default slot for the main content, plus as many named slots as you need (header, footer, sidebar, etc.).

Advanced Vue.js Slots Techniques

Scoped Slots (Slot Props)

Sometimes the child has data the parent needs to render the slot (e.g. each item in a list, or internal state). Scoped slots let the child pass props into the slot; the parent receives them and uses them in the template.

The child exposes data by binding attributes on <slot>:

<!-- Child: MyComponent.vue -->
<script setup lang="ts">
  import { ref } from "vue";
  const greetingMessage = ref("Hello from the child!");
</script>

<template>
  <div>
    <slot :text="greetingMessage" :count="1"></slot>
  </div>
</template>

The parent receives a single object (e.g. slotProps) and uses it:

<!-- Parent: App.vue -->
<template>
  <MyComponent v-slot="slotProps">
    {{ slotProps.text }} — count: {{ slotProps.count }}
  </MyComponent>
</template>

When there is only a default slot and you need props, you can use v-slot directly on the component. When you have multiple slots or need to name the default one explicitly, use <template #default="slotProps">.

Destructuring Slot Props

You can destructure the props right in v-slot for cleaner template code:

<template>
  <MyComponent v-slot="{ text, count }">
    {{ text }} — count: {{ count }}
  </MyComponent>
</template>

You can also rename the props like this: v-slot="{ text: greeting, count }". This is useful for:

  • Avoiding name collisions
  • Clarifying the meaning of the props within the parent context

Named Scoped Slots

Named slots can also pass props. The child gives a slot a name and binds data; the parent uses the same name and receives the props:

Child:

<template>
  <div>
    <slot name="header" :title="pageTitle"></slot>
    <slot :items="items"></slot>
    <slot name="footer" :year="currentYear"></slot>
  </div>
</template>

Parent:

<template>
  <MyComponent>
    <template #header="{ title }">
      <h1>{{ title }}</h1>
    </template>

    <template #default="{ items }">
      <ul>
        <li v-for="item in items" :key="item.id">{{ item.name }}</li>
      </ul>
    </template>

    <template #footer="{ year }">
      <p>© {{ year }}</p>
    </template>
  </MyComponent>
</template>

So: named slots define where content goes; scoped slots define what data the parent gets when rendering that content.

Mixing Explicit Default and Named Scoped Slots

When you have both a default scoped slot and other named slots, use an explicit default slot so the scope is clear:

<MyComponent>
  <template #default="{ message }">
    <p>{{ message }}</p>
  </template>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</MyComponent>

If you put content between <MyComponent> and </MyComponent> without <template #default>, that content does not have access to the default slot's props. Always use <template #default="..."> when you need those props.

Passing Dynamic Slot Names

Slot names can be passed dynamically using the same bracket syntax as other directives:

<template>
  <BaseLayout>
    <template v-slot:[dynamicSlotName]>
      Content for {{ dynamicSlotName }}
    </template>

    <!-- Shorthand -->
    <template #[dynamicSlotName]> ... </template>
  </BaseLayout>
</template>

Defining Dynamic Slot Names

Besides passing slot names dynamically, you can even define them dynamically. I've found this especially useful for customizing table cells for particular columns.

<template>
  <table>
    <tbody>
      <tr v-for="item in items" :key="item.id">
        <td v-for="column in columns" :key="column.key">
          <!-- Note the use of the `item-${column.key}` slot name -->
          <slot :name="`item-${column.key}`" :item="item">
            {{ item[column.key] }}
          </slot>
        </td>
      </tr>
    </tbody>
  </table>
</template>

Conditional Slots

You can use v-if along with the $slots object to conditionally render other html elements based on whether the parent provided them or not. (Sometimes useful to control slot wrapper elements, but used rarely.)

<template>
  <div v-if="$slots.header">
    <slot name="header">
      <h1>Default header</h1>
    </slot>
  </div>
</template>

Practical Scenarios for Vue.js Slots

1. Card component (header, body, footer)

A reusable card is the perfect use case for slots. It can define a fixed structure and styling but leave the content fully controlled by the parent.

AppCard.vue:

<script setup lang="ts"></script>

<template>
  <article class="rounded-lg border bg-card p-4 shadow-sm">
    <header v-if="$slots.header" class="mb-3 border-b pb-2">
      <slot name="header"></slot>
    </header>
    <div class="card-body">
      <slot></slot>
    </div>
    <footer v-if="$slots.footer" class="mt-3 border-t pt-2">
      <slot name="footer"></slot>
    </footer>
  </article>
</template>

Using v-if="$slots.header" (and $slots.footer) keeps the header/footer DOM only when the parent actually provides those slots. Parent usage:

<AppCard>
  <template #header>
    <h3>Profile</h3>
  </template>
  <p>Your bio and preferences.</p>
  <template #footer>
    <button>Edit profile</button>
  </template>
</AppCard>

2. Page layout (header, sidebar, main, footer)

A layout component that reserves named regions so the parent can fill them per page.

AppLayout.vue:

<script setup lang="ts"></script>

<template>
  <div class="app-layout">
    <header v-if="$slots.header" class="app-header">
      <slot name="header"></slot>
    </header>
    <div class="app-body">
      <aside v-if="$slots.sidebar" class="app-sidebar">
        <slot name="sidebar"></slot>
      </aside>
      <main class="app-main">
        <slot></slot>
      </main>
    </div>
    <footer v-if="$slots.footer" class="app-footer">
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

Parent (e.g. a page):

<template>
  <AppLayout>
    <template #header>
      <AppNav />
    </template>
    <template #sidebar>
      <AppSidebar />
    </template>
    <h1>Dashboard</h1>
    <p>Main page content here.</p>
    <template #footer>
      <p>© 2025 My App</p>
    </template>
  </AppLayout>
</template>

3. Data table / list with customizable rows

The child fetches or holds the list and handles layout; the parent decides how each row looks using a scoped slot.

DataTable.vue:

<script setup>
  defineProps({
    items: {
      type: Array,
      required: true,
    },
    columns: {
      type: Array,
      required: true,
    },
  });
</script>

<template>
  <table class="table">
    <thead>
      <tr>
        <th v-for="column in columns" :key="column.key">{{ column.label }}</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(item, index) in items" :key="JSON.stringify(item)">
        <slot :name="`row-${index}`" :item="item">
          <td v-for="column in columns" :key="column.key">
            <slot :name="`item-${column.key}`" :item="item">
              {{ item[column.key] }}
            </slot>
          </td>
        </slot>
      </tr>
    </tbody>
  </table>
</template>

Parent:

<script setup lang="ts">
  import MyTable from "./components/MyTable.vue";
  import { ref } from "vue";

  const tableData = ref({
    columns: [
      { key: "name", label: "Name" },
      { key: "age", label: "Age" },
    ],
    items: [
      { name: "John", age: 20 },
      { name: "Jane", age: 24 },
      { name: "Susan", age: 16 },
      { name: "Chris", age: 55 },
      { name: "Dan", age: 40 },
    ],
  });
</script>
<template>
  <MyTable :items="tableData.items" :columns="tableData.columns">
    <template #row-0>
      <td>hello</td>
      <td>hello</td>
    </template>
    <template #item-name="{ item }">
      <strong>{{ item.name }}</strong>
    </template>
  </MyTable>
</template>

Same MyTable can be reused for products, orders, etc.—only the slot content changes.

4. Form wrapper with fields and actions

A form container that provides layout and submit state handling, with slots for the fields and the actions (buttons).

FormSection.vue:

<script setup lang="ts">
  defineProps<{ title?: string; submit: () => void }>();
  const isSubmitting = ref(false);

  async function handleSubmit() {
    isSubmitting.value = true;
    await props.submit();
    isSubmitting.value = false;
  }
</script>

<template>
  <form class="form-section" @submit.prevent="handleSubmit">
    <fieldset>
      <legend v-if="title">{{ title }}</legend>
      <slot></slot>
    </fieldset>
    <div v-if="$slots.actions" class="form-actions" :class="{'pointer-events-none opacity-50': isSubmitting}">
      <slot name="actions" :isSubmitting="isSubmitting"></slot>
    </div>
  </form>
</template>

Parent:

<template>
  <FormSection title="Shipping address" @submit="onSubmit">
    <input v-model="form.address" placeholder="Address" />
    <input v-model="form.city" placeholder="City" />
    <template #actions>
      <button type="button" @click="cancel">Cancel</button>
      <button type="submit">Save</button>
    </template>
  </FormSection>
</template>

5. Tabs with dynamic slot names

Tabs can be implemented with a slot per tab; the slot name is the tab id.

AppTabs.vue:

<script setup lang="ts">
  import { ref, computed } from "vue";

  const props = defineProps<{
    tabs: Array<{ id: string; label: string }>;
  }>();

  const activeId = ref(props.tabs[0]?.id ?? "");
  const activeSlot = computed(() => activeId.value);
</script>

<template>
  <div class="tabs">
    <div class="tabs-header">
      <button
        v-for="tab in tabs"
        :key="tab.id"
        :class="{ active: activeId === tab.id }"
        @click="activeId = tab.id"
      >
        {{ tab.label }}
      </button>
    </div>
    <div class="tabs-panels">
      <template v-for="tab in tabs" :key="tab.id">
        <div v-show="activeId === tab.id">
          <slot :name="tab.id"></slot>
        </div>
      </template>
    </div>
  </div>
</template>

Parent:

<template>
  <AppTabs
    :tabs="[
      { id: 'overview', label: 'Overview' },
      { id: 'settings', label: 'Settings' },
    ]"
  >
    <template #overview>
      <p>Overview content...</p>
    </template>
    <template #settings>
      <p>Settings form...</p>
    </template>
  </AppTabs>
</template>

Here the slot names come from data (tab.id), which is a good fit for dynamic slot names.

(For an even smoother tab component design, checkout our article on Tightly Coupled Components Vue Components with Provide/Inject)

Frequently Asked Questions (FAQ)

What are slots in Vue?

Slots are placeholders a child component exposes so its parent component can inject custom HTML or template markup into it. The child defines the slot layout, while the parent supplies the rendered content.

What is the difference between default, named, and scoped slots?

  • Default slot: A single unnamed slot (<slot />) for passing general inner content.
  • Named slots: Multiple slots specified by name (<slot name="header" /> and <template #header>) to target distinct regions.
  • Scoped slots: Slots that pass data from the child component back to the parent (<slot :item="item" />), allowing the parent to control row or item markup while using child data.

How do scoped slots work in Vue 3?

The child binds data props onto the <slot> tag (<slot :item="item" />). The parent receives those props on <template #default="{ item }"> and uses them directly within the slotted markup.

Can a Vue slot have a fallback?

Yes. Anything placed between the opening and closing <slot>Fallback Content</slot> tags inside the child acts as fallback content. It renders only when the parent does not pass any content for that slot.

How do I pass a dynamic slot name?

In the child, bind the slot name (<slot :name="dynamicName" />). In the parent, target it dynamically using bracket syntax (<template #[dynamicName]>).

Vue.js Slots Summary

  • Default slot: One unnamed <slot>; parent content between the component tags goes there. Use fallback content inside <slot> for defaults.
  • Named slots: Use <slot name="..."> and <template #name> (or v-slot:name) to target specific regions (header, footer, sidebar, etc.).
  • Scoped slots: Bind attributes on <slot>; the parent receives them in v-slot="props" or #default="props" and can destructure. Use for lists, tables, and any case where the child owns the data but the parent owns the presentation.
  • Dynamic slot names: Use #[dynamicName] or v-slot:[dynamicName] when the slot name is computed or from config (e.g. layouts, tabs).
  • Conditional regions: Use v-if="$slots.name" so you only render wrapper elements when the parent actually provided that slot.

Once you're comfortable with these patterns, you can design components that stay generic and reusable while giving each screen full control over content and layout. For more structured learning, check out a course like Vue Component Design: Master Scalable Vue.js Patterns, which covers slot props and other component patterns in depth. Or you can dive deeper into components module in our flagship Vue.js 3 Master Class to build production-ready Vue 3 applications with industry-proven design patterns!

 

What are slots in Vue?

Slots are placeholders a component exposes so its parent can inject content. The child renders a slot; the parent supplies the markup where the component is used.

What is the difference between default, named, and scoped slots?

A default slot is a single unnamed hole. Named slots expose several holes you target by name. Scoped slots pass data from the child to the parent's slot content.

How do scoped slots work in Vue 3?

The child binds data on the slot tag (<slot :item="item" />), and the parent reads it with <template #default="{ item }">. It is how data tables let the parent control row rendering while the child owns the data.

Can a Vue slot have a fallback?

Yes. Markup between the slot tags is fallback content, rendered only when the parent provides nothing.

How do I pass a dynamic slot name?

Bind the name (<slot :name="dynamicName" />) and target it with <template #[dynamicName]>.

Start learning Vue.js for free

Comments

Latest Vue School Articles

5 Component Design Patterns to Boost Your Vue.js Applications

5 Component Design Patterns to Boost Your Vue.js Applications

5 essential Vue.js component design patterns, including branching components, slots usage, list organization, smart vs dumb components, and form handling - perfect for both Vue beginners and experienced developers looking to improve code maintainability and scalability.
Vibe Coding a Collaborative Editor with Comment Support with Nuxt UI and Jazz

Vibe Coding a Collaborative Editor with Comment Support with Nuxt UI and Jazz

Why I built a Nuxt + Jazz powered real time editor, how you can use it, and a list of takeaways on building with the help of AI.
VueSchool logo

Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.

More than 200.000 users have already joined us. You are welcome too!

Follow us on Social