A Comprehensive Guide to Vue Slots

Share this article

A Comprehensive Guide to Vue Slots

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! 😎

FAQs About Vue Slots

What are Vue slots?

Vue slots are placeholders inside a component that allow you to inject content into predefined areas. They enable you to create more flexible and reusable components by allowing users of the component to customize its content.

How do I define a slot in a Vue component?

To define a slot, use the <slot> element within your component template. This element serves as a placeholder where the content provided by the parent component or the default content is inserted.

Can a component have multiple slots?

Yes, a component can have multiple named slots, each identified by a unique name. This allows you to provide different content for different sections of the component.

What is a default slot in Vue?

The default slot in Vue is the slot without a name. If a component has only one slot, it is the default slot. Content passed to the component without specifying a slot name is automatically placed in the default slot.

How do I pass content to a slot from a parent component?

To pass content to a slot, use the <template> tag in the parent component and the v-slot directive. The content inside the <template> will be passed to the corresponding slot in the child component.

What is scoped slot in Vue?

Scoped slots in Vue allow the child component to access data or methods from the parent component. This is achieved by passing a function as the slot content, allowing the child component to use the parent’s data or methods.

Are there any limitations to using slots in Vue?

While slots are highly flexible, it’s important to note that they are primarily a mechanism for passing content. Direct communication between parent and child components can be achieved through props and custom events.

Ivaylo GerchevIvaylo Gerchev
View Author

I am a web developer/designer from Bulgaria. My favorite web technologies include SVG, HTML, CSS, Tailwind, JavaScript, Node, Vue, and React. When I'm not programming the Web, I love to program my own reality ;)

Vue 3
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week