Scoped Slots and Renderless Components

Intermediate
12 min

Scoped Slots and Renderless Components

Ordinary slots let a parent pass markup into a child. Scoped slots go further: the child passes data back up into the slot so the parent decides how to render it. This inversion is the foundation of flexible list, table and "renderless" components. After this lesson you will define slot props, consume them with v-slot, and build a component that provides behavior without dictating markup.

Passing Data to a Slot

A child exposes values by binding attributes on the <slot> element. Those attributes become the slot props:

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

The content inside <slot> is the fallback used when the parent provides nothing. The parent receives the slot props through v-slot (shorthand #):

vue
<template> <DataList :items="users"> <template #default="{ item, index }"> <strong>{{ index + 1 }}.</strong> {{ item.name }} ({{ item.email }}) </template> </DataList> </template>

v-slot accepts anything a function parameter can, so you can destructure, rename ({ item: user }) and give defaults. When a component has only a default slot, the directive can go straight on the component tag: <DataList :items="users" v-slot="{ item }">.

Named Scoped Slots

Each named slot can expose its own props. A typical table component provides a header slot and a per-row row slot:

vue
<!-- DataTable.vue (excerpt) --> <template> <table> <thead><tr><slot name="header" :columns="columns" /></tr></thead> <tbody> <tr v-for="row in rows" :key="row.id"> <slot name="row" :row="row" :columns="columns" /> </tr> </tbody> </table> </template> <!-- Parent --> <DataTable :rows="orders" :columns="cols"> <template #header="{ columns }"> <th v-for="c in columns" :key="c.key">{{ c.label }}</th> </template> <template #row="{ row }"> <td>{{ row.id }}</td> <td>{{ row.total }}</td> </template> </DataTable>

Slot names can be dynamic: <template #[slotName]>. The name attribute itself is reserved and is not passed as a slot prop.

Renderless Components

A renderless (or headless) component contains only logic and renders nothing of its own; it hands everything to its default slot. This separates behavior from presentation completely:

vue
<!-- MouseTracker.vue --> <script setup> import { ref, onMounted, onUnmounted } from 'vue' const x = ref(0) const y = ref(0) const update = (e) => { x.value = e.pageX; y.value = e.pageY } onMounted(() => window.addEventListener('mousemove', update)) onUnmounted(() => window.removeEventListener('mousemove', update)) </script> <template> <slot :x="x" :y="y" /> </template>

Usage: <MouseTracker v-slot="{ x, y }">Mouse is at {{ x }}, {{ y }}</MouseTracker>. The same idea powers headless UI libraries, where a Listbox or Combobox manages keyboard navigation and ARIA state while your slot content supplies the markup.

In many cases a composable (useMouse()) is a cleaner way to share the same logic, because it avoids an extra component instance. Choose a renderless component when the logic needs to be used declaratively inside a template, or when a template-only consumer (such as a CMS block) cannot import JavaScript.

Checking Slot Content

Inside <script setup>, useSlots() returns the slots object, and in templates $slots is available. This lets a component render a wrapper only when the parent supplied content:

vue
<template> <footer v-if="$slots.footer" class="card-footer"> <slot name="footer" /> </footer> </template>

Tips

  • Keep slot props flat and stable; passing a fresh object every render is fine, but keys should not change names.
  • Provide sensible fallback content so the component works with zero configuration.
  • Document each slot's props as carefully as you would document component props.
  • Combine v-for with a scoped slot to let consumers control the markup of every item.
Quick Quiz
Question 1 of 2

How does a child component pass data to the parent's slot content?

Key Takeaways

  • Scoped slots let a child pass data up to the parent's slot content via attributes on <slot>.
  • The parent receives slot props with v-slot / #name="{ ... }" and can destructure them.
  • Named slots can each expose their own props; the name attribute is reserved.
  • Renderless components separate behavior from markup; composables are often a lighter alternative.
  • $slots and useSlots() let you render wrappers only when a slot was provided.

Next lesson: The Composition API: ref, reactive, setup — the reactive primitives that power every <script setup> component.

Scoped Slots and Renderless Components - Vue.js | CodeYourCraft | CodeYourCraft