Tips to Supercharge Your Slots (Named, Scoped, and Dynamic)

Slots allow you to write very powerful, expressive components.

Although they may seem simple at first, there is a lot that you can do with them — I discover new ways of using them all the time.

In this article we'll cover six of those patterns, so that you can take your slot skills to the next level.

Here is what we'll be covering:

  1. Using Multiple Slots in a Component
  2. Adding Slots Dynamically
  3. Make Styling Much Easier
  4. Make the Default Experience™ Incredible (Using Default Content)
  5. Hiding Slots When We Don't Need Them
  6. Extending Component Behaviour

That last one, Extending Component Behaviour, is the most powerful way of using slots that I've ever seen.

Oh, before we get started, I need to say this:

This article doesn't deal specifically with scoped slots, but all of these patterns apply to scoped slots as well.

Because, at the end of the day, a slot is a still slot, whether it's named, scoped, or dynamic.

Using Multiple Slots in a Component

If one slot is great, then multiple slots are better, right?

With named slots we can create multiple slots in a component, and then access them by their unique names. Here we create a few different slots:

<!-- Slots.vue -->
<template>
<div>
<h2>Here are some slots</h2>
<slot />
<slot name="second" />
<slot name="third" />
</div>
</template>

The default slot doesn't need a name, so we can just leave it off.

When we want to apply content to these slots, we just need to add the name to the template tag:

<template>
<Slots>
This text is applied to the default slot.
<template v-slot:second>
This is applied to the second slot.
</template>
<template v-slot:third>
This is applied to the third slot.
</template>
</Slots>
</template>

It's that simple! But there are a couple of other quick tips that are useful to know.

First, the slots don't have to be in a specific order. They just need to be direct children of the component you want them to be applied to:

<template>
<Slots>
<template v-slot:third>
This is applied to the third slot.
</template>
This text is applied to the default slot.
<template v-slot:second>
This is applied to the second slot.
</template>
</Slots>
</template>

Here, we're applying the slots to the Slots component, so they're all directly underneath that component. Just like with a regular slot, you can't nest them when applying content like this.

Second, we can simplify the syntax here by using the named slots shorthand:

<template>
<Slots>
<template #third>
This is applied to the third slot.
</template>
This text is applied to the default slot.
<template #second>
This is applied to the second slot.
</template>
</Slots>
</template>

Easier to read, easier to write 👌

When do we need to use multiple slots?

We use named slots when having just a single slot doesn't give us enough options. Which happens quite a lot.

Just imagine if you could only have one prop on a component. That would make it really difficult to get much done!

For example, we could create an Input component that allows an icon to be placed either before or after the text box:

<!-- Input.vue -->
<template>
<div class="input-styling">
<slot name="icon-before" />
<input ...>
<slot name="icon-after" />
</div>
</template>

When using this component, we can either place an icon before the input:

<template>
<Input>
<template #icon-before>
<CoolIcon />
</template>
</Input>
</template>

Or we can place it after, depending on which slot we use:

<template>
<Input>
<template #icon-after>
<CoolIcon />
</template>
</Input>
</template>

If we could only have one slot, this would be extremely difficult to pull off.

But this is really just the beginning of named slots. We can also create them dynamically, which opens up a whole new level to us.

Adding Slots Dynamically

Not only can we put props into our templates with curly braces {{ prop }}, but Vue also let's us have dynamic arguments in our directives, allowing us to write very flexible code:

<template>
<MyComponent
v-bind:[propName]="somePropValue"
v-on:[eventName]="someEventHandler"
/>
</template>

If that syntax looks unfamiliar because you've been using the short-hands for so long, here's your refresher. Using :prop="someValue" and @event="eventHandler" are the short-hands for v-bind:prop="someValue" and v-on:event="eventHandler".

v-bind:prop="someValue" -> :prop="someValue"v-on:event="eventHandler" -> @event="eventHandler"

But dynamic arguments don't end with props and events. We can also use dynamic arguments with slots:

<!-- Slots.vue -->
<template>
<div>
<h2>Here are some slots</h2>
<!-- Create the slots dynamically -->
<template v-for="slotName in list">
<slot :name="slotName" />
</template>
</div>
</template>
export default {
setup() {
return {
listOfSlots: ['first', 'second', 'third'],
};
},
};

We use the template tag here so we can use the v-for without rendering anything extra to the DOM. You could use a div tag to wrap this instead, but that ends up creating unnecessary elements, unless you need it for styling or other reasons.

Applying content to these slots works exactly as you'd expect:

<template>
<Slots>
<template #first>
First slot
</template>
<template #second>
Second slot
</template>
<template #third>
Third slot
</template>
</Slots>
</template>

We can take this one step further though, and have everything be done dynamically, including applying the content.

First, we'll modify our Slots component to grab the list of slots through a prop instead of from it's own state:

<!-- Slots.vue -->
<template>
<div>
<h2>Here are some slots</h2>
<!-- Create the slots dynamically -->
<template v-for="slotName in list">
<slot :name="slotName" />
</template>
</div>
</template>
export default {
props: {
list: {
type: Array,
required: true,
},
},
};

Second, we'll put the list in our parent component, and then modify the parent component to apply content to the slots dynamically as well:

<template>
<Slots :list="listOfSlots">
<template
v-for="slotName in listOfSlots"
v-slot:[slotName]
>
<span :key="slotName">We're in {{ slotName }}, now!</span>
</template>
</Slots>
</template>
export default {
setup() {
return {
listOfSlots: ['first', 'second', 'third'],
};
},
};

This is the important bit:

<template
v-for="slotName in listOfSlots"
v-slot:[slotName]
>

We use the v-for to iterate over each of the slot names. The line v-slot:[slotName] then picks up on the new value of slotName each time around, applying the content to our three different slots.

(Don't forget the key on the child of the template tag, either!)

When would you want to use dynamically named slots?

We need dynamic slots because we often write components that are highly dynamic.

Slots are great for overriding and extending behaviour of a component. Often this behaviour is fairly static, so we can write our slots statically.

For example, a page layout component only needs a header and footer slot, so we can write those in to the template:

<!-- PageLayout.vue -->
<template>
<div class="page-wrapper">
<slot name="header" />
<PageNavigation />
<PageContent />
<slot name="footer" />
</div>
</template>

Other components, like a datatable, a form, or a menu component, are much more dynamic. What they render is highly dependent on the data props that you pass to them.

In cases like these, the slots also need to be created dynamically.

Make Styling Much Easier

Often when writing slots, we want some default styling applied to it, so the content put inside that slot always looks the same.

We could force whoever is applying the slot content to do this, but that creates extra work — and more opportunities to mess things up:

<template>
<StyledSlot>
<!-- Must first wrap in a div to apply styles -->
<div class="default-styles">
This is the content for the slot.
</div>
</StyledSlot>
</template>

Instead, we can simply move the div with the styling inside of our StyledSlot component. Then those default-styles will always be applied:

<!-- StyledSlot.vue -->
<template>
<div class="default-styles">
<slot />
</div>
</template>

Now using that slot with the default styles is much more straightforward:

<template>
<StyledSlot>
<!-- No unnecessary div -->
This is the content for the slot.
</StyledSlot>
</template>

It seems simple, but there's a catch:

Not all slots should do this. We want to avoid adding extra CSS where it doesn't belong.

We've all spent way too much time fighting with margin or padding being applied where it shouldn't.

We don't want to resort to overriding styles to remove padding, adding in negative margins, or doing other hacky things to get your app looking right.

You need to figure out what kind of slot you're dealing with.

There are two kinds:

  1. Layout slots
  2. Content slots

Layout slots are there just to place markup in the right spot on the page. They don't add any styling at all.

Something like this PageLayout component has two layout slots, the header and footer slots:

<!-- PageLayout.vue -->
<template>
<div class="page-wrapper">
<slot name="header" />
<PageNavigation />
<PageContent />
<slot name="footer" />
</div>
</template>

Here we don't care about how those are styled. All we care about is that the header content goes before everything else, and that the footer content goes after everything.

On the other hand, content slots include default styling. This is probably the most common use case.

For example, if we have a Button component, we want the slot content to be styled in a very particular way. We'll want padding, a specific colour, and maybe some other things, too:

<!-- Button.vue -->
<template>
<button class="default-button-styling">
<slot />
</button>
</template>

We definitely don't want to have different styling every time a button is used!

Make the Default Experience™ Incredible (Using Default Content)

A great way to improve your components is to make them super easy to re-use.

To do this we'll borrow an idea from writing props, and apply it to writing slots. We can do this because props and slots are actually very similar to each other.

Props let us pass data into a component.

And if you think about it, that's exactly what slots are doing, too.

Knowing this, we can apply many of the things we know about props to help us use slots better. In fact, slots and props are so closely related, that in some cases we can convert them back and forth.

One of the things that you should always be doing with props is including a default value:

export default {
props: {
limit: {
type: Number,
default: 3,
},
},
};

You want to do this because it makes the behaviour of the component more consistent and predictable.

If there is no default value, it will be undefined if it's not set otherwise. This can cause all sorts of weird behaviour in our code if we don't anticipate this.

With this component, if we don't provide a value for the limit prop, we know it will be 3. A value that actually makes sense, instead of undefined.

computed: {
allowRetry() {
return this.tries < this.limit;
},
},

What's less than undefined? lessdefined?

If we didn't provide a default, the undefined value would probably break our code somewhere (because undefined isn't exactly a number). In practice, I often use a default value of undefined, because it's a useful value in Javascript. But the key is that the component is written to expect that value.

Another important reason to add defaults is that it's also easier for the developer using this component.

There are many props that only need to be set every once in a while. In our example, perhaps a limit of 3 is fine 90% of the time.

If you don't add a default value to a prop, it's one more thing that the developer using this component has to think about. They have to figure out why the component is throwing weird errors, then figure out what value would make sense.

And this process repeats every time the component is being used!

It's so much easier if you just put in that default value from the start.

This exact line of reasoning applies to slots as well. It's so much easier for anyone using the component if you provide default content in your slots.

So just do it!

Here's how:

<!-- DefaultContent.vue -->
<template>
<div>
<slot>
<!-- Default content goes between the slot tags -->
This is the default content
</slot>
</div>
</template>

If we apply a slot or not, we still get something rendered to the page:

<template>
<DefaultContent>
Overriding the default content
</DefaultContent>
<DefaultContent />
</template>
Overriding the default content
This is the default content

Keep in mind that the default content can be far more complex than a plain string of text. You can encapsulate really complex behaviour and logic into a component that's used for the default behaviour of that slot:

<!-- DefaultContent.vue -->
<template>
<div>
<slot>
<!-- You can override this complex behaviour
with your own, if needed -->
<ReallyComplexDefaultBehaviour />
</slot>
</div>
</template>

Now, sometimes you'll want to create a slot that is usually empty. In this case, you don't want default content.

You'll want something else instead.

Hiding Slots When We Don't Need Them

First I'll show you how, then we'll get into why you'd want to hide slots.

Every Vue component has a special $slots object with all of your slots in it. The default slot has the key default, and any named slots use their name as the key:

const $slots = {
default: <default slot>,
icon: <icon slot>,
button: <button slot>,
};

But this $slots object only has the slots that are applied to the component, not every slot that is defined.

Take this component that defines several slots, including a couple named ones:

<!-- Slots.vue -->
<template>
<div>
<h2>Here are some slots</h2>
<slot />
<slot name="second" />
<slot name="third" />
</div>
</template>

If we only apply one slot to the component, only that slot will show up in our $slots object:

<template>
<Slots>
<template #second>
This will be applied to the second slot.
</template>
</Slots>
</template>
$slots = { second: <vnode> }

We can use this in our components to detect which slots have been applied to the component, for example, by hiding the wrapper element for the slot:

<template>
<div>
<h2>A wrapped slot</h2>
<div v-if="$slots.default" class="styles">
<slot />
</div>
</div>
</template>

Now the wrapper div that applies the styling will only be rendered if we actually fill that slot with something.

If we don't use the v-if, we would end up with an empty and unnecessary div if we didn't have a slot. Depending on what styling that div has, this could mess up our layout and make things look weird.

So why do we want to be able to conditionally render slots?

There are three main reasons to use a conditional slot:

  1. When using wrapper divs to add default styles
  2. The slot is empty
  3. If we're combining default content with nested slots

For example, when we're adding default styles, we're adding a div around a slot:

<template>
<div>
<h2>This is a pretty great component, amirite?</h2>
<div class="default-styling">
<slot >
</div>
<button @click="$emit('click')">Click me!</button>
</div>
</template>

However, if no content is applied to that slot by the parent component, we'll end up with an empty div rendered to the page:

<div>
<h2>This is a pretty great component, amirite?</h2>
<div class="default-styling">
<!-- No content in the slot, but this div
is still rendered. Oops. -->
</div>
<button @click="$emit('click')">Click me!</button>
</div>

Adding that v-if on the wrapping div solves the problem though. No content applied to the slot? No problem:

<div>
<h2>This is a pretty great component, amirite?</h2>
<button @click="$emit('click')">Click me!</button>
</div>

So far we've covered a lot about getting more out of your slots, but I left the best one for last.

Extending Component Behaviour

Great, we know how to use slots really well, but what do we actually use them for? Is there a pattern we can use to know when we should be reaching for slots instead of props?

Yes, there is!

One of the best ways to think about how to use slots (at least, in my own opinion), is to think of them extending the behaviour of a component.

Let's take this Button component for example. It has a prop that let's us tell it what icon to display:

<!-- Button.vue -->
<template>
<button @click="$emit('click')">
<IconComponent :name="icon" />
<slot />
</button>
</template>
export default {
props: {
icon: {
type: String,
default: null
},
},
};

We would use this component like this:

<template>
<Button icon="danger">
Don't click this button!
</Button>
</template>

But what happens if we want to use an icon that's not supported by the IconComponent?

Or an icon that is styled slightly differently?

Do we have to write an entirely new ButtonWithDifferentIcon component?

Nope!

We just use the extension pattern to make that part of the component extendable. This is done by wrapping it in a slot, so we keep the default behaviour the same:

<!-- Button.vue -->
<template>
<button @click="$emit('click')">
<slot name="icon">
<!-- If the slot has no content, we'll
render the IconComponent instead -->
<IconComponent :name="icon" />
</slot>
{{ text }}
</button>
</template>

Now we can provide our own icon component to this Button component, without resorting to two separate components:

<template>
<Button text="This button has a special icon">
<template #icon>
<SpecialIconComponent />
</template>
You can click this button.
</Button>
</template>

I spend an entire module of my course, Reusable Components, on this idea of extension.

If you combine this idea with named slots, dynamic slots, or any of these other ideas, you can do a lot of really interesting stuff!