Passing through slots in Vue

If you’re keeping your Vue component sizes small, there’s a good chance you’ll need to implement a wrapper component at some point.

If you’re only utilising the default slot for these, it can be as simple as putting a <slot /> tag inside your wrapper component. However, there’s a bit more effort involved if you need to pass through named slots to your base component

Written By

Craig Riley

Craig is a full-stack developer who mainly deals in JavaScript and PHP. Frameworks include VueJS Laravel, Wordpress, Nuxt and Quasar. He also likes sysadmin stuff until it goes horribly wrong.

Why use wrapper components?

Here’s a couple of examples as to use cases for wrapper components we’ve implemented in southcoastweb projects:

Input Components

If you’re creating inputs components, they’re going to share a lot of presentation logic and potentially validation logic. You obviously don’t want to have to copypaste this into your text input, your password input, your select input, your textarea input et al. So you’d probably want to have a BaseInput component with this common presentation logic, e.g.

2 <div class="input">
3 <div class="input__content">
4 <div class="input__prepend">
5 <slot name="prepend"></slot>
6 </div>
7 <div class="input__element">
8 <slot></slot>
9 </div>
10 <div class="input__append">
11 <slot name="append">
12 </div>
13 </div>
14 <div class="input__messages">
15 <slot name="messages"></slot>
16 </div>
17 </div>

Obviously there’d be more involved in a base input, but since this article is about slots, we’ll focus on those.


In some of our backend UI projects, we use the Vuetify UI framework. In admin panels, the Datatable is probably the most common component used after Button. To allow these datatables to interact with our backend with as little boilerplate code as possible, we use a wrapper component for <v-data-table>. Vuetify’s data table has a lot of slots we may want to use so once more, we need a way to pass through those slots.

The wrong way

Consider the following in our input wrapper example. This is a potential TextInput component:

2 <BaseInput class="input--text">
3 <template #prepend><slot name="prepend"></slot></template>
4 <input type="text" />
5 <template #append><slot name="append"></slot></template>
6 <template #messages><slot name="message"></slot></template>
7 </BaseInput>
10<script setup>
11import BaseInput from "components/BaseInput.vue";

This would work, but that’s a lot of boilerplate code, the kind we want to avoid. Additionally, it requires us to statically declare every slot we want to pass through to the BaseInput component. In cases like the DataTable wrapper, this simply isn’t possible since we’ll often be dynamically declaring slots that we want to use based on the columns in the table. We’d also have to keep every input component up-to-date with the slots available in the base component.

A better way

Instead, we can use the $slots property available in the <template> and iterate over it.

2 <BaseInput class="testing">
3 <template v-for="(_, name) in $slots" #[name]="slotData">
4 <slot :name="name" v-bind="slotData" />
5 </template>
6 <input type="text" />
7 </BaseInput>

Let’s break down how this works:

The <template> tag is how we tell the BaseComponent that we want to put the content inside the tag into its slot by the same name.

We do a v-for="(_, name) in $slots" to iterate over all the slots passed to our InputText component. The _ is the render function of the slot itself. Since we’re not using this, we can call it anything that signifies this fact. The name property is the one we’re interested in.

The #[name] is where we define the name of the slot we want this <template> to represent. # is Vue shorthand for v-slot:

Then inside the <template>, we’re using a <slot> tag to tell our InputText component to create a slot by the same name (:name) as the template and to pass through any slot content to the BaseInput component.

Finally, there’s the slotData. This is Vue’s way of passing props between the wrapping slot and the inner slot (i.e. from BaseInput to InputText). For more information on this, see the Scoped Slots section of the Vue docs.

In render functions

This whole process is ridiculously easy inside render functions, you barely have to do anything at all:

1import { h } from "vue";
2import BaseInput from "./BaseInput.vue";
4export default {
5 setup(props, { slots }) {
6 return () =>
7 h(
8 BaseInput,
9 {
10 class: "input--text",
11 },
12 slots
13 );
14 },

and that’s it. All the slotProps are automatically passed through in addition to the slots themselves.

Differences in Vue 2

SlotProps came pretty late to the party in Vue 2. When they arrived, it was necessary to have two entirely separate properties to handle slots without props and slots with props, thus the $slots and $scopedSlots properties were required to awkwardly co-exist inside Vue 2.

So in Vue 2, to get the same effect as above, you’d need something like:

2 <BaseInput>
3 <slot v-for="(_, name) in $slots" :name="name" :slot="name" />
4 <template v-for="(_, name) in $scopedSlots" :slot="name" slot-scope="slotData">
5 <slot :name="name" v-bind="slotData" />
6 </template>
7 <input type="text" />
8 </BaseInput>

This separately iterates over the old $slots property, and then the $scopedSlots property and passes through the slotProps data.

More Tutorials

Here are some more tutorials that we think might be helpful for you. Feel free to contact us if there's anything you might need

Composable Design Patterns in Vue 3

Back in the days of Vue 2, the mixin was king. Had some code you wanted to reuse in separate components? Create a mixin, import and register it in the component and its provided props, data and methods would magically be available. The problem with mixins was that magic. You'd often end up calling methods or referring to props that you had no way of knowing existed without visiting the mixin file and double-checking what it provided. If you actually wanted unit testing or type checking for that component, you faced an even tougher battle. Enter: the composable.

Automatic Vue Component Registration with Vite

Developing a large web application is a constant struggle to not keep writing the same things over and and over again. Part of this can be the need to continually import your components within a Vue application. While there are some great tools to help automatically register all of your components globally (like unplugin-vue-components), the downside is that you can't register them using async components. This means that every single component has to be loaded before your app can start, even ones which aren't on the page being accessed. For a public-facing website like southcoastweb this was a non-starter, so we had to come up with a better way.

Using Vue's class function in your own composables

Inside your &lt;template&gt;, you may be used to stacking lots of conditionally applied classes onto elements. You can add dynamic classes (e.g. .btn--${props.type}), arrays of conditional if/else classes (e.g. props.error ? 'has-error' : 'is-valid') or even whole objects of conditionals (e.g. { 'is-disabled': props.disabled }. But what do we do if we want to shift that logic to our &lt;script&gt; block? While undocumented (and thus not a guaranteed API, so tread carefully), Vue internally uses a pretty simple function to make all this magic happen, and you can import it too.

Automating your service and repository patterns in Laravel with command generators

"Why spend 20 seconds doing something when you can spent 4 hours automating it," goes the proverb. See any professional proverbists around any more? No, because they've all been automated. Also it's fun. Whatever your programming pattern in Laravel, chances are that the php artisan make:x command is going to leave you high and dry on occasion. That's why it can be useful to have your own commands available to cover those gaps. We're going to go with the example of the "Service" pattern below.

How to Create a Simple Button Component in Figma

In this tutorial, we’ll create a simple button using Figma’s built-in component system.

Copyright 2007 - 2024 southcoastweb is a brand of DSM Design.