Accessibility

[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…

Fotis Adamakis
Fotis Adamakis
Senior Software Engineer / Technical Writer
3 min read
January 8, 2024

[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.vue The base component responsible for reducing code duplication and maintaining a unified look and feel across the application
  • *_UseCase_*Modal.vue For 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

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

With this in mind let’s start implementing AppModal.vue

The component will have 3 parts:

  • A named slot body with 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

You can test it live and take a look at the source code on GitHub.

[Vue 3] Creating a Reusable Modal Component

ⓘ This article is part of a Base Component Implementation series:

Any recommendations for future additions? Please leave a comment below.

Fotis Adamakis

Fotis Adamakis

Senior Software Engineer / Technical Writer

Experienced software engineer writing about front end architecture, accessibility, system design, and developer productivity. Lessons from building and maintaining large-scale frontend applications, with a focus on practical patterns that make codebases easier to understand, scale, and evolve.

Barcelona, Spain