• Fri, Mar 2026

Building a Reusable Modal with Vue Slots: Real-World Case Study

Building a Reusable Modal with Vue Slots: Real-World Case Study

In this hands-on case study, we’ll build a fully reusable modal system in Vue using slots. You’ll learn how to leverage default, named, and scoped slots to create a flexible UI component that can handle dynamic content, actions, and layouts. By the end, you’ll master Vue slots by applying them to a real-world project.

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:

  • show prop controls modal visibility.
  • @click.self ensures clicking outside closes the modal, but not when clicking inside.
  • Default named slots for header and footer, 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.

Step 4: Scoped Slots for Dynamic Footer Actions

Let’s make the footer smarter by passing actions from the child back to the parent. We’ll use a scoped slot for this.

Update Modal.vue Footer

<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.

This website uses cookies to enhance your browsing experience. By continuing to use this site, you consent to the use of cookies. Please review our Privacy Policy for more information on how we handle your data. Cookie Policy