Skip to the solution
MediumFrontend10 min read

What are slots in Vue?

457 views

The problem

Question presented to candidate: "Our design system has a Card component, and every team wants something different in the header and footer. We also have a DataTable where each team formats cells differently. How would you use slots to support all of that without adding a prop for every variation?"

What a strong answer should cover:

  • A slot is a placeholder in the child (<slot />) that the parent fills with its own template content; the content is compiled in the parent's scope.
  • Named slots (<slot name="footer">, filled with <template #footer>) for multiple regions, and fallback content between the <slot> tags.
  • Scoped slots: the child passes data back as slot props (<slot :item="item">), and the parent receives it with #item="{ item }". This is how a list or table owns the loop while the parent owns the markup.
  • Slots compile to functions the child calls when it renders, so slot content is lazy and re-renders with the child.
  • $slots (or useSlots()) lets the child check whether a slot was provided, for example to skip an empty header wrapper.
  • Typing slots with defineSlots() in TypeScript, and the template rule that mixing v-slot on the component with named <template> slots is a compile error.

Clarifying questions expected:

  • "Should the card render its header wrapper when no header is passed?"
  • "Does the table need to hand row data back to the caller's cell template?"

Code / implementation expected: Yes β€” a component with a default and a named slot with fallback, and a scoped slot used by the parent.

  • slots

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

10 min read Β· 9 sections

Read the code

VueA card with named slots and fallback, a scoped-slot list, and a lazily rendered slot
<script setup>
import { ref, defineComponent, h } from "vue";

// A card with a default slot, a named "footer" slot with fallback content,
// and a header that only renders when the parent actually passed one.
const Card = defineComponent({
  setup(_, { slots }) {
    return () =>
      h("article", { class: "card" }, [
        slots.header ? h("header", slots.header()) : null,
        h("div", { class: "body" }, slots.default?.()),
        h("footer", slots.footer ? slots.footer() : "No actions"),
      ]);
  },
});

// A list that owns the data and the loop, but lets the parent decide how each row looks.
const TodoList = defineComponent({
  props: { items: Array },
  setup(props, { slots }) {
    return () =>
      h("ul", props.items.map((item, index) =>
        h("li", { key: item.id }, slots.item ? slots.item({ item, index }) : item.text)
      ));
  },
});

// A panel that is closed: its slot function is never called.
const Collapsed = defineComponent({
  props: { open: Boolean },
  setup(props, { slots }) {
    return () => h("div", { class: "panel" }, props.open ? slots.default() : "(closed)");
  },
});

const todos = ref([
  { id: 1, text: "Write the slot docs", done: true },
  { id: 2, text: "Review the PR", done: false },
]);
const open = ref(false);
function expensive() {
  console.log("parent slot content evaluated");
  return "panel content";
}
</script>

<template>
  <Card>
    <template #header>Weekly report</template>
    <p>Default slot content, written in the parent.</p>
    <template #footer><button>Share</button></template>
  </Card>

  <Card><p>A card with no header and no footer.</p></Card>

  <TodoList :items="todos">
    <template #item="{ item, index }">
      <span :class="{ done: item.done }">{{ index + 1 }}. {{ item.text }}</span>
    </template>
  </TodoList>

  <Collapsed :open="open">{{ expensive() }}</Collapsed>
  <button class="toggle" @click="open = !open">toggle panel</button>
</template>
VueCard.vue and TodoList.vue: the same slots written as SFC templates
<!-- Card.vue -->
<template>
  <article class="card">
    <header v-if="$slots.header"><slot name="header" /></header>
    <div class="body"><slot /></div>
    <footer><slot name="footer">No actions</slot></footer>
  </article>
</template>

<!-- TodoList.vue -->
<script setup>
defineProps({ items: Array });
</script>
<template>
  <ul>
    <li v-for="(item, index) in items" :key="item.id">
      <slot name="item" :item="item" :index="index">{{ item.text }}</slot>
    </li>
  </ul>
</template>

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 55 of 92 in Vue.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Vue.js questions
Up nextMedium

Why was Reactivity Transform ($ref, $computed) removed in Vue 3.4?

Continue