Hook — 06 : 07 a.m., Medellín ↔ Panamá City code jam
Damp mountain air drifted through my window as Luis, pair-programming from a rooftop cowork in Panamá, complained, “Our card component needs a custom footer on one page, but not the others — and props are turning into spaghetti.” Ten minutes later we replaced a half-dozenv-ifs
with a clean named slot, pushed the change, and Core Web Vitals stayed rock-solid. Luis still made his 7 a.m. surf lesson. That fix-and-ride moment sets the stage for today’s deep dive into Vue 3’s most underrated power feature: slots.
Why Slots Matter for Component Design
Slots let you inject markup into a child component while keeping logic and styling encapsulated. Use them to:
- Eliminate prop explosion for layout tweaks
- Compose UIs without wrapper div hell
- Allow scoped data to flow downward without tight coupling
- Localize design tokens while sharing behavior
Once you master named, scoped, and dynamic slots you’ll build truly reusable components that survive redesigns and refactors — even when your team’s spread from Bogotá to the Dominican Republic.
Quick Vocabulary Refresher
Term | Plain-English Meaning |
---|---|
Default slot | Unnamed hole; receives root children |
Named slot | Hole identified by name attribute |
Scoped slot | Slot that exposes data (v-slot="{…}" ) to the parent |
Dynamic slot | Slot name generated at runtime |
1. Default & Named Slots — The 80 % Case
Building a Flexible Card
vueCopyEdit<!-- components/BaseCard.vue -->
<template>
<article class="card">
<header><slot name="header" /></header>
<section class="body">
<slot />
</section>
<footer><slot name="footer" /></footer>
</article>
</template>
<style scoped>
.card {border:1px solid #ccc;border-radius:.5rem;padding:1rem;}
</style>
Usage
vueCopyEdit<BaseCard>
<template #header>
<h3>Pricing</h3>
</template>
<p>$15/month — unlimited pão de queijo.</p>
<template #footer>
<button>Subscribe</button>
</template>
</BaseCard>
Unnamed slot fills the body; named slots override header and footer when needed.
2. Scoped Slots — Passing Data Upward
A Pagination Component That Emits Page Objects
vueCopyEdit<!-- components/Pager.vue -->
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{ items:number; perPage:number }>();
const pages = computed(() =>
Array.from({ length: Math.ceil(props.items / props.perPage) }, (_, i) => ({
number: i + 1,
from: i * props.perPage,
to: (i + 1) * props.perPage - 1,
}))
);
</script>
<template>
<nav class="pager">
<slot :pages="pages" />
</nav>
</template>
Consuming with v-slot
vueCopyEdit<Pager :items="42" :perPage="10" v-slot="{ pages }">
<button
v-for="p in pages"
:key="p.number"
@click="currentPage = p.number"
>
{{ p.number }}
</button>
</Pager>
The parent decides markup, while the child supplies data — perfect separation of concerns.
3. Dynamic Slots — Rendering Based on Runtime Keys
Tab Component Where Each Panel Is a Slot
vueCopyEdit<!-- components/Tabs.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue';
const active = ref('');
onMounted(() => active.value ||= Object.keys(useSlots()).[0] ?? '');
</script>
<template>
<ul class="tab-head">
<li
v-for="name in Object.keys($slots)"
:key="name"
:class="{ active: active === name }"
@click="active = name"
>{{ name }}</li>
</ul>
<div class="tab-body">
<slot :name="active" />
</div>
</template>
Parent Side
vueCopyEdit<Tabs>
<template #Brazil>
<p>Caipirinhas & code.</p>
</template>
<template #Colombia>
<p>Arepas & Vue.</p>
</template>
<template #Mexico>
<p>Tacos & TypeScript.</p>
</template>
</Tabs>
The slot keys (Brazil
, Colombia
, Mexico
) become both tab labels and content sources — no prop arrays, no JSON config.
Remote-Work Insight Box
While migrating a fintech dashboard from São Paulo, we needed to inject bank-specific action buttons into a shared transaction row. Dynamic slot names derived from bank.id
let each micro-frontend expose custom markup without touching the core table component, slashing review cycles by 30 %.
Performance & Accessibility Checkpoints
- Slot content is compiled once — no perf penalty when toggling named slots.
- Avoid slot props that mutate — keep them readonly to prevent anti-patterns.
- Keyboard flow — ensure dynamic tabs update
aria-selected
androle="tabpanel"
. - Lighthouse — check for heading hierarchy loss when injecting headers via slots.
Common Pitfalls & Fast Fixes
Gotcha | What Happens | Remedy |
---|---|---|
Forgot # in template syntax | Slot renders as literal <template> | Use #footer not footer |
Slot prop undefined | Scoped slot missed destructuring | Use v-slot="slotProps" then slotProps.data |
Dynamic slot key missing | Tab body empty | Set default via onMounted or fallback <slot /> |
Re-render storm | Passing whole object as prop | Destructure or toRefs before slot to avoid new identity |
Call-Out Cheat Sheet
API / Concept | One-liner Purpose |
---|---|
<slot /> | Default content placeholder |
#name="props" | Named and scoped slot |
useSlots() | Runtime access inside child |
Object.keys($slots) | Build menus from available slots |
<template v-slot:[key]> | Dynamic slot binding |
CLI Nuggets
Command | What It Does |
---|---|
vue-tsc --noEmit | Catches missing slot prop types |
npm i @vueuse/core | Ships ready-made composables using scoped slots (useFetch ) |
vite build --report | Ensures slot heavy components don’t bloat bundles |
Diagram (text)
Parent → passes children → Child’s <slot>
renders them → Reactivity flows back via emits or injections — physical DOM order may differ but data flow obeys component tree.
Wrap-Up
Slots turn rigid components into lego-bricks: snap in headers, footers, tooltips, or entire render trees without rewriting internals. Reach for named slots for optional regions, scoped slots when children must feed data upward, and dynamic slots to generate UIs from runtime keys. Master these patterns once and you’ll breeze through redesigns — even while coding on a hammock in Costa Rica.
Questions or slot-powered show-and-tells? Drop them below; I’ll answer between flights and late-night arepa sessions.