Slots
Let a parent put its own markup inside a child component with default, named and scoped slots.
What you'll learn
When you finish, you can do these.- 1Pass markup into a child component with a slot
- 2Fill a card's header and footer with named slots
- 3Choose how each row of a list looks with a scoped slot
- 4Spot and fix an empty footer and a slot prop never received
Picture a card on a web page: a box with a border, some padding and rounded corners. One page puts a welcome note in it, another a product, another a form. Written as a Vue component, the card is one file, Card.vue: it can own the box, but it cannot know what each page wants inside. Props, the values a page hands down to a component, suit a word or a number, not a whole piece of markup: HTML tags with their text. Slots let the page write the markup and the card decide where it goes.
The Code tab holds App.vue, a page with two cards and a list of three fruits, next to Card.vue and FancyList.vue, which draws the list. By the end you can give a card a footer of your own and mark which fruits are fresh. You need one idea from the Components and props lesson: a parent component, such as the page App.vue, imports a child component, such as Card.vue, and writes it like an HTML tag, <Card>.
The default slot
The default slot gives a child one spot for whatever the parent writes between the child's opening and closing tags. Inside the child, <slot /> marks that spot, the slot outlet, and the markup that lands there is called slot content. In the diagram, App writes Hello between <Card> and </Card>, and the word lands at the outlet inside Card.
A first version of Card.vue puts one outlet inside its box:
<template>
<div class="card">
<slot />
</div>
</template>
When App.vue writes a paragraph between <Card> and </Card>, the paragraph shows inside the card's border.
Slot content belongs to the parent: it is part of the parent's template, so it can read the parent's data and cannot see the child's variables. Card decides where the paragraph goes, never what it says.
Text between <slot> and </slot> is fallback content, shown only when the parent gives nothing for that slot. A component with no <slot /> at all drops whatever you write between its tags, and Vue does not warn.
Named slots
Named slots give a child more than one spot. In Card.vue, a header outlet and a footer outlet each carry a name, and the header outlet holds fallback text, Untitled:
<template>
<div class="card">
<header><slot name="header">Untitled</slot></header>
<slot />
<footer v-if="$slots.footer"><slot name="footer" /></footer>
</div>
</template>
The footer's v-if draws the <footer> only when there is footer content: it reads $slots, Vue's record of which slots the parent filled.
The parent fills a named slot with a <template> tag and # plus the name: <template #header>, short for v-slot:header. That <template> is a wrapper Vue does not render: only what is inside it reaches the page.
Anything outside a <template> goes to the default slot. App.vue fills every slot of the first card and gives the second a paragraph only:
<Card>
<template #header>Welcome</template>
<p>This paragraph goes into the default slot.</p>
<template #footer>Posted today</template>
</Card>
<Card>
<p>No header was given, so the fallback shows.</p>
</Card>
The first card shows Welcome in bold, the paragraph, and Posted today under a grey line. The second gives no header, so its header reads Untitled, and no footer, so the v-if leaves the <footer> out.
Scoped slots: the child hands data back
A scoped slot lets the child hand its data to the parent's markup. A card has no data of its own, so a list shows this better.
FancyList.vue receives App's three fruits, Mango, Pear and Fig, as an items prop and draws one row for each with v-for. In each row it passes the fruit, item, and its position, index, to its slot as attributes, called slot props:
<template>
<ul>
<li v-for="(item, index) in items" :key="item.id">
<slot :item="item" :index="index">{{ item.name }}</slot>
</li>
</ul>
</template>
The {{ item.name }} inside the outlet is its fallback, for a page that gives no row markup.
In App.vue, the parent receives the slot props on a <template> tag for the default slot, #default. Braces pick them out by name: { item, index } gives each one a plain name. index counts from 0, so the row number is index + 1:
<FancyList :items="fruits">
<template #default="{ item, index }">
<strong>{{ index + 1 }}.</strong> {{ item.name }}
</template>
</FancyList>
The page shows 1. Mango, 2. Pear and 3. Fig, each number in bold. FancyList keeps the loop and the <li>; App decides how a row looks.
What goes wrong
Two mistakes are common with slots: an empty wrapper left on the page, and slot content that reaches for the child's data.
The first is Card's footer without its v-if. An outlet inside a wrapper element leaves the wrapper behind when the parent fills nothing:
<footer><slot name="footer" /></footer>
The second card in App.vue, which fills no footer, ends in an empty strip: a grey line with nothing under it. Draw the wrapper only when the slot has content:
<footer v-if="$slots.footer"><slot name="footer" /></footer>
The second is in App. Slot content belongs to the parent, so it cannot reach FancyList's loop variable, item, on its own:
<FancyList :items="fruits">
<strong>{{ item }}</strong>
</FancyList>
Each row shows an empty bold tag, and the preview warns Property "item" was accessed during render but is not defined on instance. App has no item; only FancyList does.
Receive it from the slot props. When the default slot is the only one you fill, v-slot can sit on the component tag itself, with no <template>:
<FancyList :items="fruits" v-slot="{ item }">
<strong>{{ item.name }}</strong>
</FancyList>
Each row shows its fruit's name in bold.
What to remember
<slot />marks where the parent's content lands. Text inside<slot>is the fallback, shown when the parent gives nothing.- Named slots are filled with
<template #name>. Content outside any<template>goes to the default slot, and content for a component with no slot is dropped without a warning. - A scoped slot passes values with
<slot :item="item" />, and the parent reads them with#default="{ item }". - Wrap optional slot markup in
v-if="$slots.name"so no empty element is left behind. - Slot content belongs to the parent. It sees the parent's data, and the child's data only when the child passes it as slot props.
Try it
Open the Code tab and change the starter code. Each task says what you should see.- Give the second Card in
App.vuea footer: add<template #footer>Updated yesterday</template>inside it. A grey line with Updated yesterday appears at the bottom of that card. - Add a
freshslot prop. InFancyList.vue, add:fresh="item.days < 3"to the<slot>. In App, change the template to#default="{ item, index, fresh }"and add<em v-if="fresh">Fresh</em>after the name. Mango and Fig get the word Fresh; Pear, six days old, does not. - In
Card.vue, deletev-if="$slots.footer"from the<footer>, then remove the footer you added to the second card in task 1. That card now ends in an empty strip with a line across its top: a footer with nothing in it. Put thev-ifback and the strip goes away.