Named Slots

Give a component several holes, fill them with #name templates, and build a layout component out of named slots and fallback content.

Syntax<slot name="header" /> filled by <template #header>...</template> (longhand: <template v-slot:header>)

A slot is a hole in a component that the parent fills with markup. One unnamed hole is enough for a card with a body. A layout has several, and each one needs a name so the parent can say which is which.

Declaring and filling

Inside the component, <slot name="header" /> declares a hole called header. A <slot> with no name is the default slot. In the parent, each hole is filled by a <template> tag carrying v-slot:header, or the shorthand #header. The two forms compile to exactly the same thing; the short one is what you will see in most code. Any top-level content the parent passes that is not wrapped in one of those templates goes to the default slot.

Content written between the <slot> tags is fallback. It renders only when the parent passes nothing for that name, which is how a component can have an optional footer without the caller doing any work.

What trips people up

The order of the templates in the parent does not matter. The component's own template decides where each one lands. Slot names are plain strings, not expressions, unless you use the dynamic form. And v-slot is legal only on a <template> tag or on the component tag itself, which is the compile error people hit first.

Example

Example · vue
<!-- CardPanel.vue -->
<script setup lang="ts">
defineProps<{
  heading: string
}>()
</script>

<template>
  <section class="panel">
    <header class="panel-head">
      <slot name="header">
        <h3>{{ heading }}</h3>
      </slot>
    </header>

    <div class="panel-body">
      <slot />
    </div>

    <footer class="panel-foot">
      <slot name="footer">
        <p>No actions</p>
      </slot>
    </footer>
  </section>
</template>

<style scoped>
.panel {
  border: 1px solid #d0d0d0;
  border-radius: 6px;
}
.panel-head,
.panel-foot {
  padding: 0.5rem 0.75rem;
  background: #f6f6f6;
}
.panel-body {
  padding: 0.75rem;
}
</style>

When to use it

  • A page shell with header, sidebar and footer regions that every route fills differently.
  • A card component where the title area sometimes needs a badge and an avatar, not just text.
  • A settings row whose control is a checkbox on one screen and a select on another.

More examples

Filling the three slots from a parent

The panel renders a heading bar, the paragraph in the middle, and a button in the footer. Clicking the button raises the count in the label. Note that the paragraph is not wrapped in a template, so it goes to the default slot, and that the footer template is written before it yet still renders last: placement is the component's decision, not the caller's. The two templates show the shorthand and the longhand side by side.

Example · vue
<!-- ReleasePage.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import CardPanel from './CardPanel.vue'

const likes = ref(0)

function markHelpful(): void {
  likes.value += 1
}
</script>

<template>
  <card-panel heading="Release notes">
    <!-- shorthand -->
    <template #header>
      <h3>Release 2.4</h3>
    </template>

    <!-- longhand, identical to the line above -->
    <template v-slot:footer>
      <button type="button" @click="markHelpful">
        Helpful ({{ likes }})
      </button>
    </template>

    <p>Two fixes and one new export.</p>
  </card-panel>
</template>

Fallback content as the common case

Used as <tool-bar label="Invoices" /> the bar shows the label on the left and nothing on the right. Pass #start and the label disappears, because fallback content renders only when that slot is unfilled. This is the shape to reach for when nine screens out of ten want the same markup: the tenth overrides one hole instead of the component growing a prop for every variation.

Example · vue
<!-- ToolBar.vue -->
<script setup lang="ts">
defineProps<{
  label: string
}>()
</script>

<template>
  <div class="bar">
    <div class="bar-start">
      <slot name="start">
        <strong>{{ label }}</strong>
      </slot>
    </div>

    <div class="bar-end">
      <slot name="end" />
    </div>
  </div>
</template>

<style scoped>
.bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  padding: 0.5rem;
}
</style>

Discussion

  • Be the first to comment on this lesson.