← Back to blog

How to Build Modals in VueJS 3 with vue-final-modal and Tailwind 3

| VueJS

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 ongoing axios request, I prevent the user from closing the modal until the HTTP request completes. This option uses the esc-to-close and click-to-close options in vue-final-modal.
  • opened: this option uses the opened event from vue-final-modal, giving you more control when the modal becomes visible. For example, I use this to set focus on an input.

You are not restricted to these options. Consult the documentation and add any additional options that meet your needs.

I added the title as a property rather than a slot. This works for my use case, but if you need more flexibility, switch it to a slot.

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-modal operates 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-modal documentation for more details on useModal.

Summary

  • Base Modal component -- create a reusable Modal.vue that wraps VueFinalModal with your default layout and shared props like title, closable, and opened.
  • 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-modal relies on events and the useModal composable to open, close, and communicate between modals and parent components.
  • Closable control -- use the closable prop to prevent users from dismissing the modal during async operations like HTTP requests.
Share