Table of contents [Show]
- Introduction: Why Build a Modal with Vue Slots?
- Step 1: Setting Up the Project
- Step 2: Creating the Modal Shell
- Step 3: Using the Modal Component
- Step 4: Scoped Slots for Dynamic Footer Actions
- Step 5: Building Multiple Modal Variants
- Step 6: Best Practices for Slot-Based Modals
- Step 7: Extending the System to Layouts
- Conclusion: Slots as the Secret Weapon
Introduction: Why Build a Modal with Vue Slots?
If you’ve ever worked on a Vue application, you’ve probably had to build a modal—a pop-up window that overlays the app to show information, forms, or confirmations. Modals are everywhere: sign-in dialogs, confirmation popups, product previews, or even entire checkout flows.
Now, you could hardcode every modal, but that leads to duplication. Instead, the smarter approach is to build a reusable modal system. With Vue’s slot system, we can build a single <Modal> component that handles layout, animations, and accessibility, while letting the parent pass in any content it wants. Think of it like creating a universal stage (the modal shell), and each scene (slot content) changes depending on the story you’re telling.
Step 1: Setting Up the Project
For this tutorial, I’ll assume you’re working with Vue 3 and have a fresh project created with create-vue or Vite. If not, quickly set one up:
npm init vue@latest reusable-modal-demo
cd reusable-modal-demo
npm install
npm run dev
Once your project is running, you’re ready to start coding.
Step 2: Creating the Modal Shell
Let’s build the basic Modal.vue component. This will define the structure, backdrop, and slot areas where we’ll inject content.
Modal.vue
<template>
<div v-if="show" @click.self="close">
<div >
<header >
<slot name="header">Default Header</slot>
</header>
<main >
<slot>Default Body Content</slot>
</main>
<footer >
<slot name="footer">
<button @click="close">Close</button>
</slot>
</footer>
</div>
</div>
</template>
<script>
export default {
name: "Modal",
props: {
show: {
type: Boolean,
required: true
}
},
emits: ["close"],
methods: {
close() {
this.$emit("close");
}
}
}
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0,0,0,0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-container {
background: #fff;
border-radius: 8px;
max-width: 600px;
width: 100%;
padding: 16px;
}
.modal-header,
.modal-footer {
padding: 8px 0;
border-bottom: 1px solid #eee;
}
.modal-footer {
border-top: 1px solid #eee;
border-bottom: none;
}
</style>
Explanation:
showprop controls modal visibility.@click.selfensures clicking outside closes the modal, but not when clicking inside.- Default named slots for
headerandfooter, plus a default slot for body content.
Step 3: Using the Modal Component
Now let’s use our reusable modal inside App.vue.
App.vue
<template>
<div>
<h1>Vue Reusable Modal Example</h1>
<button @click="showModal = true">Open Modal</button>
<Modal :show="showModal" @close="showModal = false">
<template #header>
<h2>Custom Modal Header</h2>
</template>
<p>This is the main content of the modal body.</p>
<template #footer>
<button @click="showModal = false">Cancel</button>
<button>Confirm</button>
</template>
</Modal>
</div>
</template>
<script>
import Modal from "./components/Modal.vue";
export default {
components: { Modal },
data() {
return {
showModal: false
}
}
}
</script>
Result: You’ve got a reusable modal with dynamic header, body, and footer. Each instance can look different, yet the shell remains the same.
Let’s make the footer smarter by passing actions from the child back to the parent. We’ll use a scoped slot for this.
<footer >
<slot name="footer" :close="close">
<button @click="close">Close</button>
</slot>
</footer>
App.vue Usage
<template #footer="{ close }">
<button @click="close">Cancel</button>
<button @click="handleConfirm">Confirm</button>
</template>
Now the modal gives the parent access to the close method via scoped slot props, making the component even more flexible.
Step 5: Building Multiple Modal Variants
With this setup, you can easily create different types of modals—confirmation dialogs, form modals, or info modals—without duplicating code.
Confirmation Modal
<Modal :show="showConfirm" @close="showConfirm = false">
<template #header>
<h3>Confirm Action</h3>
</template>
<p>Are you sure you want to delete this item?</p>
<template #footer="{ close }">
<button @click="close">No</button>
<button @click="deleteItem">Yes, Delete</button>
</template>
</Modal>
Form Modal
<Modal :show="showForm" @close="showForm = false">
<template #header>
<h3>Contact Us</h3>
</template>
<form @submit.prevent="submitForm">
<label>Name: <input type="text" v-model="name" /></label>
<label>Email: <input type="email" v-model="email" /></label>
<button type="submit">Send</button>
</form>
<template #footer="{ close }">
<button @click="close">Cancel</button>
</template>
</Modal>
Step 6: Best Practices for Slot-Based Modals
- Keep accessibility in mind: Add
role="dialog"and focus traps for production apps. - Always provide sensible defaults: Like the fallback “Close” button in the footer.
- Use scoped slots for flexibility: Let the parent control actions.
- Style for responsiveness: Ensure modals adapt to smaller screens.
Step 7: Extending the System to Layouts
The same approach works beyond modals. You can create layout systems with slots. For example, a DashboardLayout component with slots for sidebar, header, and content can handle many variations of dashboards while keeping the base layout consistent.
DashboardLayout.vue
<template>
<div >
<aside >
<slot name="sidebar">Default Sidebar</slot>
</aside>
<div >
<header><slot name="header">Default Header</slot></header>
<section><slot>Default Content</slot></section>
</div>
</div>
</template>
Conclusion: Slots as the Secret Weapon
By building this reusable modal, we’ve explored default slots, named slots, and scoped slots in a real-world case study. We’ve seen how slots let us distribute content dynamically, create flexible APIs, and reduce code duplication. Whether you’re designing modals, layouts, or data tables, slots are the secret weapon that transforms your Vue components into adaptable UI building blocks.






