Vue.js 3 Component Slots Tutorial

In this Vue tutorial we learn how one component can embed content in another with slots.

We cover how to create and use slots, default fallback content, named slots, slot props and the $slot instance variable.

Lesson Project

If you want to follow along with the examples, you will need an app generated by the Vue CLI as well as the following extra component.

  • src/components/PostCard.vue

The project should look similar to the following.

Example: project
project-folder/
├── src/
|   ├── components/
|   |   └── PostCard.vue
|   └── App.vue

We want to nest two instances of PostCard inside the root App component with a prop called content .

Example: src/App.vue
<template>
  <post-card content="Post Card 1" />
  <post-card content="Post Card 2" />
</template>

<script>
import PostCard from './components/PostCard.vue'

export default {
  components: { PostCard }
}
</script>

In PostCard we’ll register the prop and use it a div.

Example: src/components/PostCard.vue
<template>
  <div class="card">{{ content }}</div>
</template>

<script>
export default {
  props: ['content']
}
</script>

<style>
.card {
  max-width:250px;text-align:center;
  margin:2rem auto;padding:.6rem .8rem;
  border: .1rem solid #94A3B8;color:#64748B;
}
</style>

What are Slots

In an earlier lesson, we learned that we can reuse components by passing data from the parent component to the child with props .

While props are great, it does create a strict parent-child relationship. The parent can pass values to the child, but the child will always be in control of the HTML.

Slots allow the parent component to embed content, including HTML elements, in a child component.

For example, our current project allows us to pass a string to the content prop and it will show in the card. But what if we want that content to be made up of other HTML elements, like a title, image and a button.

That’s where slots come in. A slot is a tag we can specify in the child component, then overwrite with content from inside the parent.

How to create and use a slot

To create a slot, we add an open-and-close slot tag in the child component’s template where we want the overriding content from the parent to appear.

Syntax: child slot definition
<template>
  <slot></slot>
<template>

When we use the component in the parent’s template, we add the overriding content between the component’s open and close tags.

Syntax:
// component instance changes
// from a self-closing tag
<component-name />

// to an open-and-close tag
<component-name>
  // overriding content
</component-name>

To demonstrate, we’ll replace the prop in PostCard with a slot element. We’ll also remove the config object because we don’t need it anymore.

Example: src/components/PostCard.vue
<template>
  <div class="card">
    <slot></slot>
  </div>
</template>

<style>
.card {
  max-width:250px;text-align:center;
  margin:2rem auto;padding:.6rem .8rem;
  border: .1rem solid #94A3B8;color:#64748B;
}
</style>

In root App component’s template block, we’ll remove the prop from the component instances and change the self-closing tags to open-and-close tags.

For the overriding content, we chose a heading, image from Lorem Picsum and a button for the first component instance, and just a picture for the second instance.

Example: src/App.vue
<template>
  <post-card>
    <h2>Card Title</h2>
    <img src="https://picsum.photos/200">
    <button>Read more...</button>
  </post-card>

  <post-card>
    <img src="https://picsum.photos/200">
  </post-card>
</template>

<script>
import PostCard from './components/PostCard.vue'

export default {
  components: { PostCard }
}
</script>

If we save the files and take a look at the browser, we can see that the cards each have their own content, but they still have the styling from the child component.

Default slot content

Vue allows us to specify default content in the slot if the parent doesn’t provide any overriding content.

We specify default slot content inside the open-and-close slot tags of the child component.

Example:
<template>
  <slot>
    // HTML elements here
  </slot>
<template>

As an example, let’s say that we want our card to show a message if no content is defined for it in the parent.

We will start in the PostCard child component and add a heading and paragraph between the slot tags with some text.

Example: src/components/PostCard.vue
<template>
  <div class="card">
    <slot>
      <h2>404</h2>
      <p>Post Not Found!</p>
    </slot>
  </div>
</template>

<style>
.card {
  max-width:250px;text-align:center;
  margin:2rem auto;padding:.6rem .8rem;
  border: .1rem solid #94A3B8;color:#64748B;
}
</style>

In the root App component, we’ll add two PostCard instances in the template block. One instance is a self-closing tag and the other is an open-and-close tag without anything inside.

Example:
<template>
  <post-card />

  <post-card></post-card>

  <post-card>
    <h2>Card Title</h2>
    <img src="https://picsum.photos/200">
    <button>Read more...</button>
  </post-card>

  <post-card>
    <img src="https://picsum.photos/200">
  </post-card>
</template>

<script>
import PostCard from './components/PostCard.vue'

export default {
  components: { PostCard }
}
</script>

If we save the files and take a look in the browser, the two new instances will show the text we defined in the child component’s slot tag.

Named slots

Sometimes we want to have predefined structure and content in the child component, and only control some of the content from the parent component.

As an example, the card’s button can be the same for all the instances, but the heading and image should be different.

Vue allows us to have multiple slots in a single component. However, to ensure that the content we provide in the parent doesn’t override the wrong slot in the child, Vue requires us to name each slot element.

To name a slot, we attach the name attribute to the slot element and specify a unique name as its value.

Syntax: slot name
<slot name="slotName"></slot>

We are allowed to leave one slot from a group without a name. Vue will treat the unnamed slot as the default slot and give it a name of “default” behind the scenes.

Syntax: default slot
<slot name="slot1"></slot>
<slot name="slot2"></slot>
<slot></slot> // treated as 'default'

The “default” slot and default slot content are not the same.

  • The “default” slot is simply a slot that we don’t manually give a name to. Vue will give it its name behind the scenes which happens to be “default”.
  • Default slot content is specified between the slot tags. An unnamed slot will not act as a location to store the component’s default content.

In the parent, we will now need to specify which slot we provide content for.

We surround the content we want to override the slot with, with an open-and-close template tag. Then we attach the v-slot directive to that template element to indicate to Vue which slot we want to override.

The v-slot directive uses a colon, followed by the name of the slot we want to override.

Syntax: v-slot
<component-name>
  <template v-slot:slot_name>
    // content to override
    // child component
  </template>
</component-name>

As mentioned before, we are allowed to leave one slot unnamed in the child component. Vue then gives it the name “default” behind the scenes. When we override the unnamed slot, we use “default” as the name.

Syntax: unnamed override
<component-name>
  <template v-slot:slot_name>

  </template>
  <template v-slot:default>

  </template>
</component-name>

To demonstrate, let’s change our previous example to have a named and default slot.

Example: src/components/PostCard.vue
<template>
  <div class="card">
    <h2>
      <slot name="cardTitle">Fallback Title</slot>
    </h2>
    <slot>Fallback content</slot>
    <button>Read more...</button>
  </div>
</template>

<style>
.card {
  max-width:250px;text-align:center;
  margin:2rem auto;padding:.6rem .8rem;
  border: .1rem solid #94A3B8;color:#64748B;
}
</style>

Next, we’ll fill in the slots in the root App component.

Example: src/App.vue
<template>

  <post-card>
    <template v-slot:cardTitle>Named slots</template>
    <template v-slot:default>
      <img src="https://picsum.photos/200">
    </template>
  </post-card>

</template>

<script>
import PostCard from './components/PostCard.vue'

export default {
  components: { PostCard }
}
</script>

If we save the files and take a look in the browser, the card shows the title, image and button as expected. But this time, some elements of the card are predefined in the child.

The v-slot directive shorthand

Because v-slot is used so much, the Vue team has created a shorthand for it.

All we need to do is replace v-slot: with an # (octothorp).

Syntax: v-slot shorthand
<template #slotName>Content</template>
<template #default>Default slot</template>

As an example, let’s use the shorthand in the root App component.

Example: src/App.vue
<template>

  <post-card>
    <template #cardTitle>Named slots</template>
    <template #default>
      <img src="https://picsum.photos/200">
    </template>
  </post-card>

</template>

<script>
import PostCard from './components/PostCard.vue'

export default {
  components: { PostCard }
}
</script>

If we run the example in the browser, everything still works as expected.

How to communicate from a child component to a parent with Slot Props

In the Component Events lesson , we learned how to emit data from a child component to its parent with event binding. Slot props (known as Scoped Slots in Vue 2) is a way to do the same when we are working with slots.

We make a prop available to the parent by binding it to a slot in the child with the v-bind directive. As its value, we specify the data we want to send to the parent.

Syntax: slot props
<slot name="slot_name" :propName="data_to_send"></slot>

The v-slot directive can receive the data from a prop in its value. The data will be available as an object so we can use dot notation to access the prop.

Syntax: slot prop value object
<component-name>
  <template v-slot:slotName="slotPropsObject">
    {{ slotPropsObject.propName }}
  </template>
</component-name>

As an example, let’s say the title for our card is defined in the config object of PostCard . We can use a slot prop to send the title to the parent to be used there.

Example: src/components/PostCard.vue
<template>
  <div class="card">
    <slot name="title" :pTitle="postTitle"></slot>
  </div>
</template>

<script>
export default {
  data() {
    return { postTitle: 'Post Title' }
  }
}
</script>

<style>
.card {
  max-width:250px;text-align:center;
  margin:2rem auto;padding:.6rem .8rem;
  border: .1rem solid #94A3B8;color:#64748B;
}
</style>

Now we can receive the data in the root App parent component. The slot prop object name can be anything you want, we chose the letter p .

Example: src/App.vue
<template>
  <post-card>
    <template v-slot:title="p">
      {{ p.pTitle }}
    </template>
  </post-card>
</template>

<script>
import PostCard from './components/PostCard.vue'

export default {
  components: { PostCard }
}
</script>

If we save the files and take a look at the browser, we will see the post title that was defined in the child component. So the data was successfully sent from the child to the parent.

A more practical example would be to send the current iteration of a v-for loop to the parent. The parent will then be able to have control over each iteration.

How to get slot details with the $slot instance variable

Vue provides us with the special $slot instance variable that contains more information about the slot(s) in our component.

To access a named slot, we use the $slot instance variable with the slot’s name. If a slot is unnamed, we specify the name as “default”.

Syntax: $slot
// named slot
$slots.slot_name

// unnamed slot
$slots.default

We can use this information for some defensive programming. For example, let’s say we only want to show the blog card when it has a title.

Example: src/components/PostCard.vue
<template>
  <div class="card" v-if="$slots.title">
    <slot name="title"></slot>
  </div>
</template>

<style>
.card {
  max-width:250px;text-align:center;
  margin:2rem auto;padding:.6rem .8rem;
  border: .1rem solid #94A3B8;color:#64748B;
}
</style>

In the root App component, let’s add two more PostCard instances at the top of the template block. Once as a self-closing element, and once as an open-and-close element.

Example: src/App.vue
<template>
  <post-card/>
  <post-card></post-card>

  <post-card>
    <template v-slot:title>Blog Card Title</template>
  </post-card>
</template>

<script>
import PostCard from './components/PostCard.vue'

export default {
  components: { PostCard }
}
</script>

If we save the files and take a look in the browser, only the card with the static title shows up.