7 Apr 2023

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

Out of all the things I’ve tried to create, modals have been one of the most frustrating. As someone who isn’t well-versed in front-end development, I had no idea where to begin with building them.

While I believe that vue-final-modal is the top choice for a modal library in VueJs, I won’t be comparing it with other solutions here. Rather, I’d like to share my experience and offer guidance on how to create modals in an easy and effective manner.

Let’s get started:

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')

Finally, add the ModalsContainer in the App.vue file:

<template>
	<!-- // -->
  <ModalsContainer />
</template>

<script setup>
import { ModalsContainer } from 'vue-final-modal'
</script>

In my project, I have organized the modals by placing them into a folder aptly named modals.

Feel free to create the modalsfolder in your project wherever you see fit. Personally, I have placed mine in components/modals, but the location is entirely up to your preference.

For the next step, you’ll need to create a Vue component with the name Modal.vue. However, it’s entirely you’re up to you whether you want to stick with this name or choose a different one. This file will act as the primary Modal component for other modals.

Did I say other modals?

Indeed, I create a separate modal component for every modal, which helps me keep them organized and under control.

For example, I have a modal named 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 is permitted to close or not. For instance, during an ongoing axios request, I prefer to prevent the user from closing the modal until the http request completes. This option utilizes the esc-to-close and click-to-close options in vue-final-modal.
  • opened: this option utilizes the opened event from vue-final-modal giving me more control when the modal becomes visible. For example, I utilize this option to set the focus on the input.

You are not restricted to these options, so feel free to instruct the documentation and add any additional options that meet your needs.

Please note that I have added the title as a property and not a slot. This approach satisfies my requirements, but you may find it necessary to switch to using a slot. So, feel free to make that change if you see fit.

Let’s create the actual modal, here is one of the modals in my project CreateNewFolderModal.vue:

<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 quite straightforward as it uses the Modal component and emits a created event once the folder is created. There isn’t much else to it.

It’s worth noting that vue-final-modal operates on events, which implies that you need to emit an event to control it later on.

The last step it to use the modal in your project.

Here is how I use CreateNewFolderModal.vue in my project:

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

Let’s demystify the code.

The useModal function provides the open and close functions, which are utilized to show and hide the modal, respectively.

To improve the code’s readability, I assigned openCreateNewFolder and closeCreateNewFolder as aliases for the open and close functions. This simple technique enhances the code’s clarity.

In the component property, we specify the modal component to use.

To handle the created event, I utilize the attrs property, which is provided by vue-final-modal. Using on and specifying the event name is all that’s required.

Another fascinating option available in useModal is slots, which enables you to modify the slots within your modal.

Please refer to the vue-final-modal documentation in order to know more about the use of useModal.

That’s it. Thanks for reading.