Day 8: Slots & Dynamic Components
Welcome to Day 8! Props are great for passing data, but what if you want to pass HTML or template content? That’s where Slots come in.
Slots: Content Projection
Imagine a <Card> component. You want it to wrap whatever you put inside it with a nice shadow and border.
Child (Card.vue)
<template>
<div class="card">
<!-- The content from parent goes here -->
<slot>Default content if nothing provided</slot>
</div>
</template>
<style scoped>
.card { border: 1px solid #ddd; padding: 20px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
</style>
Parent (App.vue)
<Card>
<h2>My Title</h2>
<img src="..." />
<button>Click me</button>
</Card>
Named Slots
What if a component has multiple “holes”? (e.g., Header, Body, Footer).
Child (Layout.vue)
<template>
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot> <!-- Default slot -->
</main>
<footer>
<slot name="footer"></slot>
</footer>
</template>
Parent
<Layout>
<template #header>
<h1>The Header</h1>
</template>
<p>The main content...</p>
<template #footer>
<p>Copyright 2025</p>
</template>
</Layout>
Note: # is shorthand for v-slot:.
Dynamic Components
Sometimes you want to switch between components based on a variable (like Tabs).
<script setup lang="ts">
import { ref } from 'vue'
import Home from './Home.vue'
import About from './About.vue'
import Contact from './Contact.vue'
// Shallow Ref is better for components to avoid Reactivity overhead
import { shallowRef } from 'vue'
const activeComponent = shallowRef(Home)
</script>
<template>
<button @click="activeComponent = Home">Home</button>
<button @click="activeComponent = About">About</button>
<button @click="activeComponent = Contact">Contact</button>
<div class="view">
<!-- Special built-in element -->
<component :is="activeComponent" />
</div>
</template>
KeepAlive
By default, when you switch away from Home, it is destroyed. If you want to “cache” it (keep scroll position, form input):
<KeepAlive>
<component :is="activeComponent" />
</KeepAlive>
Challenge for Day 8
- Create a
Modalcomponent. - It should have two slots:
headerandbody. - Add a generic “Close” button in the
Modalthat emits acloseevent. - In Parent, start with a button “Show Modal”.
- Clicking it shows the Modal (via
v-if). Clicking “Close” inside Modal hides it.
Solution:
Child (Modal.vue)
<script setup lang="ts">
defineEmits(['close'])
</script>
<template>
<div class="overlay" @click="$emit('close')">
<div class="modal" @click.stop>
<header>
<slot name="header">Default Header</slot>
</header>
<div class="body">
<slot></slot>
</div>
<button @click="$emit('close')">Close</button>
</div>
</div>
</template>
<style scoped>
.overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: grid; place-items: center; }
.modal { background: white; padding: 2rem; border-radius: 8px; }
</style>
Parent
<script setup lang="ts">
import { ref } from 'vue'
import Modal from './Modal.vue'
const show = ref(false)
</script>
<template>
<button @click="show = true">Open Modal</button>
<Teleport to="body">
<Modal v-if="show" @close="show = false">
<template #header>My Custom Modal</template>
<p>This is the important message content.</p>
</Modal>
</Teleport>
</template>
Wait, what is <Teleport>? It moves the Modal to the <body> tag so styles don’t conflict! A free bonus tip for today. Tomorrow, we clean up logic with Composables.