Modals are one of those things that seem simple until you actually try to build them. If you are not deeply experienced with front-end development, figuring out the right approach can be genuinely frustrating.
I consider vue-final-modal the best modal library for Vue.js. This post will not compare it to alternatives. Instead, it walks you through how to create modals in a clean, maintainable way.
First, install the package:
npm install vue-final-modal
Add it into your project (main.js):
import { createApp } from 'vue'
import { createVfm } from 'vue-final-modal'
import App from './App.vue'
const app = createApp(App)
const vfm = createVfm()
app.use(vfm).mount('#app')
Then add the ModalsContainer in the App.vue file:
<template>
<!-- // -->
<ModalsContainer />
</template>
<script setup>
import { ModalsContainer } from 'vue-final-modal'
</script>
In my project, I organize modals by placing them in a dedicated modals folder. I put mine in components/modals, but the location is up to you.
Next, create a Vue component named Modal.vue. This file acts as the base modal component that all other modals extend.
Yes -- other modals. I create a separate modal component for every modal in the application. This keeps things organized and easy to manage. For example, I have CreateNewFolderModal.vue, and so on.
Here is the Modal.vue content:
<template>
<VueFinalModal
:click-to-close="closable"
:esc-to-close="closable"
@opened="opened"
class="flex justify-center items-center"
content-class="flex flex-col max-w-xl mx-4 p-6 bg-white border rounded-lg space-y-2 shadow-xl"
>
<div>
<div class="mt-3 text-center sm:mt-5">
<h3 class="text-base font-bold text-xl leading-6 text-gray-900">
{{ title }}
</h3>
<div class="mt-2">
<slot name="content"/>
</div>
</div>
</div>
</VueFinalModal>
</template>
<script setup>
import { VueFinalModal } from "vue-final-modal";
defineProps({
title: {
type: String,
required: true,
},
closable: {
type: Boolean,
required: false,
default: true,
},
opened: {
type: Function,
required: false,
}
});
</script>
There is one slot and a few properties:
title: The modal's title, such as create a new folder.closable: determines whether the modal can be closed. For instance, during an ongoingaxiosrequest, I prevent the user from closing the modal until the HTTP request completes. This option uses theesc-to-closeandclick-to-closeoptions invue-final-modal.opened: this option uses theopenedevent fromvue-final-modal, giving you more control when the modal becomes visible. For example, I use this to set focus on aninput.
You are not restricted to these options. Consult the documentation and add any additional options that meet your needs.
I added the
titleas a property rather than aslot. This works for my use case, but if you need more flexibility, switch it to aslot.
Now, create the actual modal. Here is CreateNewFolderModal.vue from my project:
<template>
<Modal title="Create new folder" :closable="closeable" :opened="focusInput">
<template #content>
<form @submit.prevent="create">
<div class="mt-2">
<input class="input w-80" name="folderName" id="folderName" v-model="folderName" ref="folderNameInputRef" placeholder="Folder name" :disabled="isCreating"/>
</div>
<div class="mt-2">
<button class="btn" type="submit" :disabled="isCreating">Create</button>
</div>
</form>
</template>
</Modal>
</template>
<script setup>
import Modal from "@/components/Modal.vue";
import { ref } from "vue";
import folders from "@/api/folders";
const closeable = ref(true);
const isCreating = ref(false);
const folderNameInputRef = ref();
const folderName = ref();
const emit = defineEmits(['created']);
function create() {
closeable.value = false;
isCreating.value = true;
folders.createFolder(folderName.value).then((data) => {
closeable.value = true;
isCreating.value = false;
emit('created', data)
}).catch(() => {
closeable.value = true;
isCreating.value = false;
// Handle the errors
})
}
function focusInput() {
folderNameInputRef.value.focus();
}
</script>
This component is straightforward. It uses the Modal component and emits a created event once the folder is created.
vue-final-modaloperates on events, meaning you need to emit an event to control it.
The last step is to use the modal in your project.
Here is how I use CreateNewFolderModal.vue:
<template>
<div>
<button type="button" @click="openCreateNewFolder">Create new folder</button>
</div>
</template>
<script setup>
import { useModal } from "vue-final-modal";
import CreateNewFolderModal from "@/components/modals/CreateNewFolderModal.vue";
const { open: openCreateNewFolder, close: closeCreateNewFolder } = useModal({
component: CreateNewFolderModal,
attrs: {
onCreated(folder) {
// ...
closeCreateNewFolder();
},
},
});
</script>
Here is what is happening in that code.
The useModal function provides open and close functions to show and hide the modal respectively.
I assigned openCreateNewFolder and closeCreateNewFolder as aliases for the open and close functions. This improves readability when you have multiple modals in a single component.
In the component property, you specify which modal component to use.
To handle the created event, use the attrs property provided by vue-final-modal. Prefix the event name with on and capitalize it -- that is all you need.
Another useful option available in useModal is slots, which allows you to modify the slots within your modal.
Refer to the
vue-final-modaldocumentation for more details onuseModal.
Summary
- Base Modal component -- create a reusable
Modal.vuethat wrapsVueFinalModalwith your default layout and shared props liketitle,closable, andopened. - One modal per feature -- build a separate component for each modal (e.g.,
CreateNewFolderModal.vue) to keep your codebase organized. - Event-driven control --
vue-final-modalrelies on events and theuseModalcomposable to open, close, and communicate between modals and parent components. - Closable control -- use the
closableprop to prevent users from dismissing the modal during async operations like HTTP requests.