Components are the heart of modern web application development. Every app is composed of a number of components smoothly stitched together in order to work as a whole unit. These components need to be maximally flexible and reusable to allow for using them in different situations and even in different apps. One of the main mechanisms many frameworks use to meet such requirements — in partucular Vue — is called a “slot”.
Slots are a powerful and versatile content distribution and composition mechanism. You can think of slots as customizable templates (similar to PHP templates, for example) which you can use in different places, for various use cases, producing different effects. For example, in UI frameworks like Vuetify, slots are used to make generic components such as an alert component. In these kinds of components, slots are used as placeholders for the default content and any additional/optional content, such as icons, images, and so on.
Slots allow you to add any structure, style, and functionality to a particular component. By using slots, developers can drastically reduce the number of props used in a single component, making components much cleaner and manageable.
In this tutorial, we’ll explore how to harness the power of slots in the context of Vue 3. Let’s get started.
Basic Usage of Slots
Basically, Vue offers two kinds of slots: a simple slot, and a scoped slot. Let’s start with the simple one. Consider the following example:
const app = Vue.createApp({})
app.component('primary-button', {
template: `
<button>
<slot>OK</slot>
</button>`
})
app.mount('#app')
Here, we have a primary button component. We want the button’s text to be customizable, so we use the slot
component inside the button
element to add a placeholder for the text. We also want a default (fallback) generic value in case we don’t provide a custom one. Vue uses as default slot content everything we put inside the slot
component. So we just put the text “OK” inside the component. Now we can use the component like this:
<div id="app">
<primary-button></primary-button>
</div>
See the Pen
Vue 3 Slots: Basic Slot by SitePoint (@SitePoint)
on CodePen.
The result is a button with text “OK”, because we haven’t provided any value. But what if we want to create a button with custom text? In that case, we provide custom text in the component implementation like this:
<div id="app">
<primary-button>Subscribe</primary-button>
</div>
Here, Vue takes the custom “Subscribe” text and uses it instead of the default one.
As you can see, even in this simple example, we get a great amount of flexibility over how we want to present our component. But this is only the tip of the iceberg. Let’s look at a more complex example.
Building a Quote of the Day Component
Now, we’ll build a quote component which displays the quote of the day. Here’s the code:
const app = Vue.createApp({})
app.component('quote', {
template: `
<article>
<h2>The quote of the day says:</h2>
<p class="quote-text">
<slot></slot>
</p>
</article>`
})
app.mount('#app')
<div id="app">
<quote>
<div class="quote-box">
"Creativity is just connecting things."
<br><br>
- Steve Jobs
</div>
</quote>
</div>
.quote-box {
background-color: lightgreen;
width: 300px;
padding: 5px 10px;
}
.quote-text {
font-style: italic;
}
In this example, we create a title heading whose content will be constant, and then we put a slot component inside a paragraph, whose content will vary depending on the current day’s quote. When the component is rendered, Vue will display the title from the quote component followed by the content we put inside the quote tags. Also pay attention to the CSS classes used both in the quote creation and implementation. We can style our components in both ways depending on our needs.
See the Pen
Vue 3 Slots: Quote Component by SitePoint (@SitePoint)
on CodePen.
Our quote of the day component works fine, but we still need to update the quote manually. Let’s make it dynamic by using the Fav Quotes API:
const app = Vue.createApp({
data() {
return {
quoteOfTheDay: null,
show: false
};
},
methods: {
showQuote() {
axios.get('https://favqs.com/api/qotd').then(result => {
this.quoteOfTheDay = result.data
this.show = true
});
}
}
})
...
app.mount('#app')
<div id="app">
<quote>
<button v-if="show == false" @click="showQuote">Show Quote of the Day</button>
<div v-if="show" class="quote-box">
{{ quoteOfTheDay.quote.body }}
<br><br>
- {{ quoteOfTheDay.quote.author }}
</div>
</quote>
</div>
Here, we use Axios to make a call to the “Quote of the Day” API endpoint, and then we use the body
and author
properties, from the returned JSON object, to populate the quote. So we no longer need to add the quote manually; it’s done automatically for us.
See the Pen
Vue 3 Slots: Quote Component with Axios by SitePoint (@SitePoint)
on CodePen.
Using Multiple Slots
Although a single slot can be quite powerful, in many cases this won’t be enough. In a real-world scenario, we’ll often need more than one single slot to do the job. Fortunately, Vue allows us to use as many slots as we need. Let’s see how we can use multiple slots by building a simple card component.
Building a Basic Card Component
We’ll build a card component with three sections: a header, a body, and a footer:
const app = Vue.createApp({})
app.component('card', {
template: `
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>`
})
app.mount('#app')
<div id="app">
<card>
<template v-slot:header>
<h2>Card Header Title</h2>
</template>
<template v-slot:default>
<p>
Lorem ipsum leo risus, porta ac consectetur ac, vestibulum at eros. Donec id elit non mi porta gravida at eget metus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras mattis consectetur purus sit amet fermentum.
</p>
</template>
<template v-slot:footer>
<a href="#">Save</a> -
<a href="#">Edit</a> -
<a href="#">Delete</a>
</template>
</card>
</div>
In order to use multiple slots, we must provide a name for each of them. The only exception is the default slot. So, in the above example, we add a name
property for the header and footer slots. The slot with no name provided is considered default.
When we use the card
component, we need to use the template
element with the v-slot
directive with the slot name: v-slot:[slot-name]
.
See the Pen
Vue 3 Slots: Card Component by SitePoint (@SitePoint)
on CodePen.
Note: the v-slot
directive has a shorthand, which uses special symbol #
followed by the slot’s name. So, for example, instead of v-slot:header
, we can write #header
.
Named slots can also be used with third-party components, as we’ll see in the next section.
Using Named Slots with Bulma’s Card Component
Let’s take the Bulma’s Card component and tweak it a little bit:
const app = Vue.createApp({})
app.component('card', {
template: `
<div class="container">
<div class="card">
<header class="card-header">
<slot name="header"></slot>
</header>
<main class="card-content">
<slot></slot>
</main>
<footer class="card-footer">
<slot name="footer"></slot>
</footer>
</div>
</div>`
})
app.mount('#app')
.container {
width: 300px;
}
<div id="app">
<card>
<template v-slot:header>
<p class="card-header-title">
Card Header Title
</p>
</template>
<template v-slot:default>
<p>
Lorem ipsum leo risus, porta ac consectetur ac, vestibulum at eros. Donec id elit non mi porta gravida at eget metus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras mattis consectetur purus sit amet fermentum.
</p>
</template>
<template v-slot:footer>
<a href="#" class="card-footer-item">Save</a>
<a href="#" class="card-footer-item">Edit</a>
<a href="#" class="card-footer-item">Delete</a>
</template>
</card>
</div>
Here, we use the classes from the Bulma Card component as a base skeleton and add a slot for each section (header, content, footer). Then, when we add the content, everything is structured properly.
See the Pen
Vue 3 Slots: Card Component with Bulma by SitePoint (@SitePoint)
on CodePen.
Using Scoped Slots
We’ve taken a big step forward by using multiple slots, but the real horse power comes from scoped slots. They allow a parent to get access to the child data, which gives us many opportunities. Let’s see them in practice.
Building a Multipurpose List Component
To demonstrate the power of scoped slots, we’ll build a multipurpose list component that can be used in different scenarios when we need to list some data:
const app = Vue.createApp({
data() {
return {
tasks: ['Reading a book', 'Buying vegetables', 'Going for a walk']
}
}
})
app.component('list', {
props: ['items', 'name'],
template: `
<h3>{{ name }}</h3>
<ul>
<li v-for="item in items">
<slot :item="item"></slot>
</li>
</ul>`
})
app.mount('#app')
<div id="app">
<list name="My tasks list:" :items='tasks' v-slot="{ item: task }">
<span>{{ task }}</span>
</list>
</div>
Here, we create a list
component with items
and name
props. The name
will be used to give a title to the list and the items
will hold the data we want to list. In the template we add a name
prop to a heading above the list, and then we use the v-for
directive to render each single item. To expose the data from the child to the parent we bind the item as a slot attribute (<slot :item="item"></slot>
).
In the parent we have an array of tasks. When we use the list we provide a name prop, then bind the items
to the tasks
array and we use v-slot
to get access to the child data.
Note: all bound props in a slot are called slot props, and we can expose them by using v-slot:[slot-name]="slotProps"
, and then we use single prop like this: {{ slotProps.item }}
. But in this example, I use object destructuring, which is more elegant and direct way to get the object properties. Also it allows you to rename the object properties (as I did, renaming item
to task
), which is more flexible for different kinds of lists.
See the Pen
Vue 3 Slots: Multipurpose List Component – Tasks by SitePoint (@SitePoint)
on CodePen.
The purpose of our list component is to be used in different scenarios. So, let’s say we want to use it as a shopping list. Here’s how to do it:
const app = Vue.createApp({
data() {
return {
products: [
{name: 'Tomatoes', quantity: '4'},
{name: 'Cucumbers', quantity: '2'},
{name: 'Red onion', quantity: '1'},
]
}
}
})
...
app.mount('#app')
<div id="app">
<list name="My shopping list:" :items='products' v-slot="{ item: product }">
<span>{{ product.quantity }} {{ product.name }}</span>
</list>
</div>
Here, we have a list of products where each item has a name
and a quantity
property. We use the list
component almost identically as in the previous example, except that here the list item has two properties.
See the Pen
Vue 3 Slots: Multipurpose List Component – Products by SitePoint (@SitePoint)
on CodePen.
As you can see, the list component can be easily adapted to different listing use cases.
Slots vs Props
Before learning about slots (and realizing their power), many developers mainly use props for content distribution. But when we have a complex component, the number of props can increase drastically. In such cases, slots can replace the use of props, making component implementation much clearer.
To illustrate the point made above, we’ll take an example from Tailwind CSS, which uses only props to create a vacation-card
component:
const app = Vue.createApp({})
app.component('vacation-card', {
props: ['url', 'img', 'imgAlt', 'eyebrow', 'title', 'pricing'],
template: `
<div class="m-5 shadow-md w-80">
<img class="rounded" :src="img" :alt="imgAlt">
<div class="p-2">
<div>
<div class="text-xs text-gray-600 uppercase font-bold">{{ eyebrow }}</div>
<div class="font-bold text-gray-700 leading-snug">
<a :href="url" class="hover:underline">{{ title }}</a>
</div>
<div class="mt-2 text-sm text-gray-600">{{ pricing }}</div>
</div>
</div>
</div>`
})
app.mount('#app')
<div id="app">
<vacation-card
url="/vacations/cancun"
img="https://images.unsplash.com/photo-1452784444945-3f422708fe5e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=512&q=80"
imgAlt="Beach in Cancun"
eyebrow="Private Villa"
title="Relaxing All-Inclusive Resort in Cancun"
pricing="$299 USD per night"
>
</vacation-card>
</div>
See the Pen
Vue 3 Slots: Tailwind Primer with Props by SitePoint (@SitePoint)
on CodePen.
As you can see, to provide all the needed data/content, this vacation-card
component uses six props. This definitely makes it reusable, but also hard to maintain. We can produce aa much clearer and maintainable version by using slots instead of some props.
Let’s rewrite the component with slots:
const app = Vue.createApp({})
app.component('vacation-card', {
props: ['url', 'img', 'imgAlt'],
template: `
<div class="m-5 shadow-md w-80">
<img class="rounded" :src="img" :alt="imgAlt">
<div class="p-2">
<div>
<div class="text-xs text-gray-600 uppercase font-bold"><slot name="eyebrow"></slot></div>
<div class="font-bold text-gray-700 leading-snug">
<a :href="url" class="hover:underline"><slot name="title"></slot></a>
</div>
<div class="mt-2 text-sm text-gray-600"><slot name="pricing"></slot></div>
</div>
</div>
</div>`
})
app.mount('#app')
<div id="app">
<vacation-card
url="/vacations/cancun"
img="https://images.unsplash.com/photo-1452784444945-3f422708fe5e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=512&q=80"
imgAlt="Beach in Cancun"
>
<template v-slot:eyebrow>
<p>Private Villa</p>
</template>
<template v-slot:title>
<p>Relaxing All-Inclusive Resort in Cancun</p>
</template>
<template v-slot:pricing>
<p><mark>$299 USD per night</mark></p>
</template>
</vacation-card>
</div>
See the Pen
Vue 3 Slots: Tailwind Primer with Slots by SitePoint (@SitePoint)
on CodePen.
Here, we reduce the props to three. We leave props only for the metadata and use named slots for the actual content, which I think is way more logical.
Here are my general considerations about using props vs slots.
Use props when:
- the parent can only pass the data down to the child component
- the parent has no control over how the data will be rendered and can’t customize the child component
- you have a defined design, the number of variables is small, and the component is simple
- you need to provide metadata or some sort of configuration
Use slots when:
- you need to pass data from the child to the parent
- the parent can determine how the data will be rendered
- you want to pass not only data but also complex HTML markup, a particular functionality, and even other components
- you want the versatility to customize the child component from the parent
- you need more flexibility to customize the component, the component is large, and there are many variables involved
The bottom line: for best results, combine props and slots! 😀
Exploring More Slots Use Cases
At this stage, you should see the power and flexibility of slots, but there are even more useful ways to use them. Let’s explore them now.
Reusing Functionality with Slots
Slots can deliver not only content/structure but also functionality. Let’s see this in action:
const app = Vue.createApp({
data() {
return {
counter: 1
}
},
methods: {
increment() {
this.counter++
}
}
})
app.component('double-counter', {
props: ['counter'],
template: `
<p>
<slot :double="double"></slot>
</p>`,
computed: {
double() {
return this.counter * 2
}
}
})
app.mount('#app')
<div id="app">
<double-counter :counter="counter">
<template v-slot:default="{ double }">
<h2>{{ counter }} x 2 = {{ double }}</h2>
<button @click="increment">
Increment counter by 1
</button>
</template>
</double-counter>
</div>
Here, we create a double-counter
component that will double the value of a counter. To do this, we create a computed property double
which we expose to the parent by binding its value as a slot attribute. The computed takes the value of the counter
prop and doubles it.
In the parent we have a counter
data property and an increment()
method that increments it by one. When we use the double-counter
, we bind the prop counter
, then we expose the double
computed property. In the expression, we use the counter
and double
. When we click the button, the counter
is incremented by 1 and the computed property is recalculated with the new doubled value. For example, if the counter
prop is set to 3, the doubled value will be 6 (3 x 2 = 6
).
See the Pen
Vue 3 Slots: Reusing Functionality – Double Counter by SitePoint (@SitePoint)
on CodePen.
We can make this component more flexible. Let’s tweak it to multiply the counter by any custom value:
const app = Vue.createApp({
data() {
return {
counter: 1,
by: 4
}
},
methods: {
increment() {
this.counter++
}
}
})
app.component('multiply-counter', {
props: ['counter', 'by'],
template: `
<p>
<slot :multiply="multiply"></slot>
</p>`,
computed: {
multiply() {
return this.counter * this.by
}
}
})
app.mount('#app')
<div id="app">
<multiply-counter :by="by" :counter="counter">
<template v-slot:default="{ multiply }">
<h2>{{ counter }} x {{ by }} = {{ multiply }}</h2>
<button @click="increment">
Increment counter by 1
</button>
</template>
</multiply-counter>
</div>
Here, we add a prop, by
, which sets the multiplication number, and we change the double
computed property to multiply
, which multiplies the counter
by the given number.
In the parent, we add the by
data property and bind it to the by
prop. So now, if we set the by
data property to 3 and the counter
is 4, the result will be 4 x 3 = 12
.
See the Pen
Vue 3 Slots: Reusing Functionality – Multiply Counter by SitePoint (@SitePoint)
on CodePen.
Using Slots in Renderless Components
The other powerful way to use slots is to put them in a renderless component. A renderless component has no template
element. It has a render function that exposes a single scoped slot. Let’s create a renderless version of our multipurpose list:
const app = Vue.createApp({
data() {
return {
products: [
{name: 'Tomatoes', quantity: '4'},
{name: 'Cucumbers', quantity: '2'},
{name: 'Red onion', quantity: '1'},
]
}
}
})
app.component('renderless-list', {
props: ['items', 'name'],
render() {
return this.$slots.default({
items: this.items,
name: this.name
});
}
})
app.mount('#app')
<div id="app">
<renderless-list name="My shopping list:" :items="products">
<template v-slot:default="{name, items: products}">
<h3>{{ name }}</h3>
<ul>
<li v-for="product in products" :key="product.name">
{{ product.quantity }} {{ product.name }}
</li>
</ul>
</template>
</renderless-list>
</div>
Here, we create a renderless-list
component which takes name
and items
props. It also exposes a single scoped slot in the render function.
Then, in the parent we use it in a similar way as our multipurpose list component — except that, this time, the content structure is defined in the parent, which gives us more flexibility as we’ll see in the next example.
See the Pen
Vue 3 Slots: Renderless Multipurpose Component – List by SitePoint (@SitePoint)
on CodePen.
Note: in Vue 3, this.$scopedSlots
is removed and this.$slots
is used instead. Also, this.$slots
exposes slots as functions. See Slot Unification for more information.
The real power of this component is that we’re not restricted how will render the content. Let’s see how easy is to render the same content as a table:
<div id="app">
<renderless-list name="My shopping list:" :items="products">
<template v-slot:default="{name, items: products}">
<h3>{{ name }}</h3>
<table>
<tr>
<th>Quantity</th>
<th>Product</th>
</tr>
<tr v-for="product in products" :key="product.name">
<td>{{ product.quantity }}</td>
<td>{{ product.name }}</td>
</tr>
</table>
</template>
</renderless-list>
</div>
table, th, td {
border: 1px solid black;
border-collapse: collapse;
padding: 5px;
}
Here, we use the same functionality but we define different content structure.
See the Pen
Vue 3 Slots: Renderless Multipurpose Component – Table by SitePoint (@SitePoint)
on CodePen.
Conclusion
So this is all the power and flexibility of slots. As we’ve seen, they can be used in a variety of use cases, which allows us to produce highly reusable and versatile components. When combined with props, slots give us all we need to create complex components and apps. Slots are easy to use but extremely powerful. If you haven’t used them before, I think now is the perfect time to start doing so. The best place to start or continue this journey is the official Slots Documentation. Good luck! 😎