[Vue 3] Creating a Reusable Modal Component
Modals are an essential building block of most web applications. They might seem tricky to implement at first, but the truth is, with Vue…
[Vue 3] Creating a Reusable Modal Component
Modals are an essential building block of most web applications. They might seem tricky to implement at first, but the truth is, with Vue and some Flexbox magic, it’s not only doable but surprisingly easy.
Let’s implement a Base Modal Component together.
The architecture will be the following:
AppModal.vueThe base component responsible for reducing code duplication and maintaining a unified look and feel across the application*_UseCase_*Modal.vueFor each use case, we will have a dedicated component responsible for specifying the contents of the modal and handling any actions.- Each page can import one of these components and should handle its visibility.
![[Vue 3] Creating a Reusable Modal Component](/images/-Vue-3--Creating-a-Reusable-Modal-Component-ac4adbe88a18/img-4.png)
The Base Modal
To create a modal we need two divs and some CSS.
<div class="modal">
<div class="modal-content">Content</div>
</div>
.modal {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 2;
background-color: $color-backdrop;
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
flex-basis: 600px;
padding: spacing(4);
background-color: $color-white;
border-radius: $border-radius;
}
The outer element has a fixed position and is it stressed to fill the whole screen and create a backdrop effect. It also sets itself as flex and aligns the content in the middle.
The inner element sets a maximum width and some styling according to our style guide. It’s important to do it here and not in every use case modal to reduce code duplication and maintain the same UI across the whole application.
![[Vue 3] Creating a Reusable Modal Component](/images/-Vue-3--Creating-a-Reusable-Modal-Component-ac4adbe88a18/img-5.png)
With this in mind let’s start implementing AppModal.vue
The component will have 3 parts:
- A named slot
bodywith all the content. - An optional named slot with a
title. - A button to close the Modal.
<script setup lang="ts">
import AppIcon from "@/components/AppIcon.vue";
const emit = defineEmits(["close"]);
function closeModal() {
emit("close");
}
</script>
<template>
<Teleport to="body">
<div class="modal" @click="closeModal">
<div class="modal-content" @click.stop>
<div class="title" v-if="$slots.title">
<slot name="title" />
</div>
<AppIcon
icon="x"
size="4x"
@click="closeModal"
clickable
class="close"
/>
<slot name="body" />
</div>
</div>
</Teleport>
</template>
<style lang="scss" scoped>
.modal {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
background-color: $color-backdrop;
}
.modal-content {
position: relative;
max-width: 600px;
width: 100%;
padding: spacing(4);
margin: 0 spacing(3);
background-color: $color-white;
border-radius: $border-radius;
}
.title {
font-size: $font-size-3;
font-weight: 700;
margin-bottom: spacing(2);
}
.close {
position: absolute;
top: spacing(3);
right: spacing(3);
}
</style>
Let’s see what is going on step by step:
For the close action, we import and use the AppIcon.vue component
We define an emit that is used in the action above and when the backdrop is clicked.
We are using the built-in Teleport directive to mount this content at the end of the body element. This is to avoid weird behavior with other elements using fixed position or z-index. More details in [Vue official documentation](https://vuejs.org/guide/built-ins/teleport.html#basic-usage).
Lastly, we declare two named slots title and body.
Use Case Modal
Let’s now see how we can consume this generic component. ProfileEditModal.vue imports AppModal and fills the default slot with a title and the body with whatever is appropriate. In this case, we are using a form from the previously published article about AppInput .
Lastly, we need to handle the emitted closing events.
<script lang="ts" setup>
import AppModal from "@/components/AppModal.vue";
import ProfileEditForm from "@/features/profile/ProfileEditForm.vue";
const emit = defineEmits(["close"]);
function closeModal() {
emit("close");
}
</script>
<template>
<AppModal @close="closeModal">
<template #title> Edit Profile </template>
<template #body>
<ProfileEditForm @submitted="closeModal" />
</template>
</AppModal>
</template>
You can see how easy and clean this approach can be.
Final Demo
Finally, let’s see how the application can consume this modal. We need a reactive variable to hold the modal open state and don’t forget to listen to the close event.
<script setup lang="ts">
import ProfileEditModal from "@/features/profile/ProfileEditModal.vue";
import { ref } from "vue";
const isModalOpen = ref(false);
</script>
<template>
<main>
<h1>Base Modal Demo</h1>
<button @click="isModalOpen = true">Edit profile</button>
<ProfileEditModal v-if="isModalOpen" @close="isModalOpen = false" />
</main>
</template>
![[Vue 3] Creating a Reusable Modal Component](/images/-Vue-3--Creating-a-Reusable-Modal-Component-ac4adbe88a18/img-2.png)
You can test it live and take a look at the source code on GitHub.
![[Vue 3] Creating a Reusable Modal Component](/images/-Vue-3--Creating-a-Reusable-Modal-Component-ac4adbe88a18/img-6.png)
ⓘ This article is part of a Base Component Implementation series:
- AppIcon.vue
- AppInput.vue
- AppModal.vue
Any recommendations for future additions? Please leave a comment below.

