Home / Blog / The Complete Guide to Vue Slots
The Complete Guide to Vue Slots

The Complete Guide to Vue Slots

Daniel Kelly
Daniel Kelly
Updated: February 12th 2026

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, slots are the right tool. This guide walks you through the basics, advanced techniques, and plenty of practical scenarios you'll use every day.

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 components 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
)

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.

Start learning Vue.js for free

Daniel Kelly
Daniel Kelly
Daniel is the lead instructor at Vue School and enjoys helping other developers reach their full potential. He has 10+ years of developer experience using technologies including Vue.js, Nuxt.js, and Laravel.

Comments

Latest Vue School Articles

Using Pretext in Vue to Build Variable-Height UI Without Layout Thrash

Using Pretext in Vue to Build Variable-Height UI Without Layout Thrash

Learn how to use Pretext in Vue to measure multiline text without hidden DOM probes, forced reflow, or brittle getBoundingClientRect loops.
Daniel Kelly
Daniel Kelly
Generating Random IDs in Vue.js

Generating Random IDs in Vue.js

How Vue 3.5’s useId() composable gives you stable, unique DOM IDs for forms and accessibility—without manual counters or hydration bugs.
Daniel Kelly
Daniel Kelly
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

© All rights reserved. Made with ❤️ by BitterBrains, Inc.