What are Slots?

Vue introduces a reserved element called <slot> that allows us compose child elements in a component. This behaves similar React’s rendering of this.props.children.

Here’s a quick example:

<!-- Component Definition - Card.vue -->
<template>
  <div class="card'>
    <slot></slot>
  </div>
</template>

<!-- Usage - App.vue -->
<Card>
  Coffee Every Day!
</Card>

Vue also gives us a way render multiple slots that can be referenced by naming the slot:

<!-- Component Definition - Card.vue -->
<template>
  <div class="card'>
    <slot name="title'>
    <slot></slot> <!-- This is the default slot if no name given -->
    <slot name="actions'>
  </div>
</template>

<!-- Usage - App.vue -->
<Card>
  👋 I will be rendered in default slot!
  <h2 slot="title">This is a Title</h2>
  <button slot="actions">Action Button</button>
</Card>

Note that you can use any valid element including custom components for the slot targets. Also, the ordering of the elements that target the slot can be rearranged, but will always render in the appropriate slot.

Scoped Slots

Adam Wathan has a fantastic article describing scoped slots:

“Scoped slots are just like regular slots but with the ability to pass parameters from the child component up to the parent/consumer.”

We get an added abilty to pass parameters, even methods, to the child. In the below example, <Confirmable> is the parent wrapping a <button> element. The behavior is that by clicking the button, there will be a confirmation dialog (yes/no) and on confirming with a yes, it will proceed to call a method:

<template>
  <div>
    <Confirmable @confirm='confirm'>
      <button slot-scope='props' @click='props.show'>
        Delete My Account
      </button>
    </Confirmable>
  </div>
</template>

The <button> is rendered within <Confirmable>s default slot, but notice the additional slot-scope='props' on the button. This is coming from <Confirmable> which can give it any kind of property to components it renders.

Before we dig into how <Confirmable> is implemented, think about how we would implement a confirm dialog otherwise. We’d need to track visibility state of that dialog. Maybe it would look like:

<template>
  <div>
    <button @click='confirmVisible = true'>
      Delete My Account
    </button>
    <ConfirmDialog :visible='confirmVisible' @confirm='confirm' @cancel='confirmVisible = false'/>
  </div>
</template>

<script>
export default {
  data() {
    return {
      // Here we need to track visibility state
      confirmVisible: false
    };
  },
  methods: {
    confirm() {
      // Actually delete account
    }
  }
};
</script>

In most cases, tracking a confirmation dialog’s state is outside the concern of the component using it. We would only be concerned about: 1) When we want to show the dialog 2) What happens when user clicks confirm.

Here’s how <Confirmable> is implemented:

<template>
  <span class='confirmable'>
    <slot
      :visible='visible'
      :toggle='toggle'
      :show='show'
      :hide='hide'
    />
    <ConfirmDialog
      v-if='visible'
      @confirm='$emit("confirm")'
      @cancel='hide'
    />
  </span>
</template>

<script>
import ConfirmDialog from "@/components/ConfirmDialog";

export default {
  components: {
    ConfirmDialog
  },
  data() {
    return {
      visible: false
    };
  },
  methods: {
    toggle() {
      this.visible = !this.visible;
    },
    show() {
      this.visible = true;
    },
    hide() {
      this.visible = false;
    }
  }
};
</script>

Notice in the <slot> it passes props to whatever component will be rendered there, without having to know or care about what is being rendered. It gives it access to the visible state, as well as methods to invoke: toggle(), show(), hide(). These are all optional for the slot component to use. For example, our usage up above only utilized the show() method.

Composition advantages of slots

Slots are great to enable flexibility of component over time, because it can render any component or markup. And with that comes ability to pass it native vue events and props.

When designing a component with just pure props, think if you could leverage slots instead. To illustrate how pure props can evolve with the lifecycle of your app, consider the following:

Release 1.0 - Need a ConfirmDialog that can render a title:

<template>
  <div class='ConfirmDialog'>
    <h2 class='title'>{{ title }}</h2>
    <div class='body'>
      Are you sure?
    </div>
    <div class='actions'>
      <button>Confirm</button>
      <button>Cancel</button>
    </div>
  </div>
</template>
<script>
export default {
  props: ["title"]
};
</script>

Release 2.0 - ConfirmDialog needs custom text for actions

<template>
  <div class='ConfirmDialog'>
    <h2 class='title'>{{ title }}</h2>
    <div class='body'>
      Are you sure?
    </div>
    <div class='actions'>
      <button>{{ confirmText }}</button>
      <button>{{ cancelText }}</button>
    </div>
  </div>
</template>
<script>
export default {
  props: ["title", "confirmText", "cancelText"]
};
</script>

Release 3.0 - ConfirmDialog needs to optionally render an icon before title

<template>
  <div class='ConfirmDialog'>
    <h2 class='title'>
      <FancyIcon v-if='showTitleIcon'/>
      {{ title }}
    </h2>
    <div class='body'>
      Are you sure?
    </div>
    <div class='actions'>
      <button>{{ confirmText }}</button>
      <button>{{ cancelText }}</button>
    </div>
  </div>
</template>
<script>
export default {
  props: ["title", "showTitleIcon", "confirmText", "cancelText"]
};
</script>

Release 4.0 - ConfirmDialog needs pass in a custom icon and render either before or after title

<template>
  <div class='ConfirmDialog'>
    <h2 class='title'>
      <component
        :is='titleIconComponent'
        v-if='showTitleIcon && titleIconComponentPosition==="before"'
      />
      {{ title }}
      <component
        :is='titleIconComponent'
        v-if='showTitleIcon && titleIconComponentPosition==="after"'
      />
    </h2>
    <div class='body'>
      Are you sure?
    </div>
    <div class='actions'>
      <button>{{ confirmText }}</button>
      <button>{{ cancelText }}</button>
    </div>
  </div>
</template>
<script>
export default {
  props: [
    "title",
    "showTitleIcon",
    "titleIconComponent",
    "titleIconComponentPosition",
    "confirmText",
    "cancelText"
  ]
};
</script>

Now if we used slots instead, we can ensure the component is more flexible to future requirements (open-closed principle):

<template>
  <div class='ConfirmDialog'>
    <div class='title'>
      <slot name='title'/>
    </div>
    <div class='body'>
      <slot name='body'>
        Are you sure?
      </slot>
    </div>
    <div class='actions'>
      <slot name='actions'>
        <button>Confirm</button>
        <button>Cancel</button>
      </slot>
    </div>
  </div>
</template>

We can still have some defaults for the slot content as well while keeping open for extension. Note that you can always create more specifc wrapper components. For example:

<template>
  <ConfirmDialog class='MoreSpecificDialog'>
    <div slot='title'>
      <UserIcon/> Delete User
    </div>
    <div slot='body'>
      Are you really really sure you want to do this?
    </div>
    <div slot='actions'>
      <button>OK</button>
      <button>Nope</button>
    </div>
  </div>
</template>

Summary

  • Slots are a great tool for component composition
  • Scoped slots allow you to abstract some stateful or other behavior

Further Reading