Tutorial

How To Build a Modal Component with Vue.js

DevelopmentVue.js

Introduction

Modals are a user experience convention to direct a user’s attention to a piece of content that they need to read or interact with. These tend to take the form of small blocks of content directly in the field of vision of the user with some sort of backdrop either obscuring or hiding the rest of the content on the page. If the user wishes to return to the page, they must engage with the modal or possibly dismiss it.

In this article you will learn how to create a reusable modal component using transitions and slots in Vue.js.

Prerequisites

To complete this tutorial, you will need:

Step 1 — Setting Up the Project

You can use @vue/cli to create a new Vue.js Project.

In your terminal window, use the following command:

  • npx @vue/cli create --default vue-modal-component-example

This will use the default configurations for creating a Vue.js Project.

Navigate to the newly created project directory:

  • cd vue-modal-component-example

Start the project to verify that there are no errors.

  • npm run serve

If you visit the local app (typically at localhost:8080) in your web browser, you will see a "Welcome to Your Vue.js App" message.

With this scaffolding set in place, you can begin work on the modal component.

Step 2 — Creating the Modal Component

First, inside the project directory, create a new Modal.vue file under src/components.

Let’s start by defining the template. You will need a div for the backdrop shade, a div to act as the modal box, and some elements to define its structure:

src/components/Modal.vue
<template>
  <div class="modal-backdrop">
    <div class="modal">
      <slot name="header">
      </slot>

      <slot name="body">
      </slot>

      <slot name="footer">
      </slot>
    </div>
  </div>
</template>

You could use props to provide the header, body, and footer, but using slots will allow for more flexibility.

The use of slots allow you to reuse the same modal with different types of body contents.

You may use a modal to show a message, but you may want to reuse the same modal with a form to submit a request.

Although props are usually enough to build a component, providing HTML through a prop would require us to use v-html to render it - which can lead to XSS attacks.

Here, you are using named slots to allow the use of more than one slot in the same component.

When you define a named slot, anything you identify with that name will be rendered instead of the original slot. Think of it as a placeholder. Like a placeholder, a slot can also have a default content that will be rendered when none is provided.

Because the content provided replaces the <slot> tag, in order to guarantee the sections have the classes you want, you will need to wrap each slot.

Let’s set some defaults for the slots, the wrapper elements, and the initial CSS to make it look like a basic modal.

Above the template, add the component name and method for closing the modal:

src/components/Modal.vue
<script>
  export default {
    name: 'Modal',
    methods: {
      close() {
        this.$emit('close');
      },
    },
  };
</script>

Then, modify the template to wrap the slots, provide default values, and also display the buttons for closing the modal:

src/components/Modal.vue
<template>
  <div class="modal-backdrop">
    <div class="modal">
      <header class="modal-header">
        <slot name="header">
          This is the default title!
        </slot>
        <button
          type="button"
          class="btn-close"
          @click="close"
        >
          x
        </button>
      </header>

      <section class="modal-body">
        <slot name="body">
          This is the default body!
        </slot>
       </section>

      <footer class="modal-footer">
        <slot name="footer">
          This is the default footer!
        </slot>
        <button
          type="button"
          class="btn-green"
          @click="close"
        >
          Close Modal
        </button>
      </footer>
    </div>
  </div>
</template>

Next, below the template, add styles for the component:

src/components/Modal.vue
<style>
  .modal-backdrop {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    background-color: rgba(0, 0, 0, 0.3);
    display: flex;
    justify-content: center;
    align-items: center;
  }

  .modal {
    background: #FFFFFF;
    box-shadow: 2px 2px 20px 1px;
    overflow-x: auto;
    display: flex;
    flex-direction: column;
  }

  .modal-header,
  .modal-footer {
    padding: 15px;
    display: flex;
  }

  .modal-header {
    position: relative;
    border-bottom: 1px solid #eeeeee;
    color: #4AAE9B;
    justify-content: space-between;
  }

  .modal-footer {
    border-top: 1px solid #eeeeee;
    flex-direction: column;
    justify-content: flex-end;
  }

  .modal-body {
    position: relative;
    padding: 20px 10px;
  }

  .btn-close {
    position: absolute;
    top: 0;
    right: 0;
    border: none;
    font-size: 20px;
    padding: 10px;
    cursor: pointer;
    font-weight: bold;
    color: #4AAE9B;
    background: transparent;
  }

  .btn-green {
    color: white;
    background: #4AAE9B;
    border: 1px solid #4AAE9B;
    border-radius: 2px;
  }
</style>

All these pieces put together complete your modal component. You can import this new component in App.vue and observe it in action.

Modify App.vue to change the template and add showModal, closeModal, and isModalVisible:

src/App.vue
<template>
  <div id="app">
    <button
      type="button"
      class="btn"
      @click="showModal"
    >
      Open Modal!
    </button>

    <Modal
      v-show="isModalVisible"
      @close="closeModal"
    />
  </div>
</template>

<script>
  import modal from './components/Modal.vue';

  export default {
    name: 'App',
    components: {
      Modal,
    },
    data() {
      return {
        isModalVisible: false,
      };
    },
    methods: {
      showModal() {
        this.isModalVisible = true;
      },
      closeModal() {
        this.isModalVisible = false;
      }
    }
  };
</script>

This code will import the Modal component and display a Open Modal button to interact with.

Note: In your App.js file you can optionally reference the slots and replace the default content:

src/App.js
<Modal
  v-show="isModalVisible"
  @close="closeModal"
>
  <template v-slot:header>
    This is a new modal header.
  </template>

  <template v-slot:body>
    This is a new modal body.
  </template>

  <template v-slot:footer>
    This is a new modal footer.
  </template>
</Modal>

View the application in your web browser and verify that the modal behaves as expected by opening and closing it.

Step 3 — Adding Transitions

You can make it appear to open and close smoother by using a transition.

Vue provides a wrapper component called transition that allows you to add transitions for entering and leaving. This wrapper component can be used for any element or component and they allow both CSS and JavaScript hooks.

Every time a component or an element wrapped by a transition is inserted or removed, Vue will check if the given element has a CSS transitions and will add or remove them at the right time. The same is also true for JavaScript hooks, but for this case, you will use only CSS.

When an element is added or removed, six classes are applied for the enter and leave transitions. Each of them will be prefixed with the name of the transition.

First, let’s start by adding a transition wrapper component to the modal:

src/components/Modal.vue
<template>
  <transition name="modal-fade">
    <div class="modal-backdrop">
      <div class="modal">
        ...
      </div>
    </div>
  </transition>
</template>

Now let’s add a transition for the opacity to fade slowly by using the applied classes:

src/components/Modal.vue
<style>
  ...

  .modal-fade-enter,
  .modal-fade-leave-to {
    opacity: 0;
  }

  .modal-fade-enter-active,
  .modal-fade-leave-active {
    transition: opacity .5s ease;
  }
</style>

Initially, the modal will appear hidden with an opacity property value of 0.

When the modal opens it will have the .modal-fade-enter-active class applied and a CSS transition for the opacity property will be applied over .5 seconds with the ease animation timing function.

When leaving the modal, it will do the reverse. The modal-fade-leave-active class will be applied and the modal will fade out.

View the application in your web browser and verify that the modal fades in and out.

Step 4 — Adding Accessibility

You will want to make your modal component accessible with ARIA attributes.

Adding role="dialog" will help assistive software to identify the component as being an application dialog that is separated from the rest of the user interface.

You will also need to properly label it with aria-labelledby and aria-describedby attributes.

And add aria-label attributes to the close buttons.

src/components/Modal.vue
<template>
  <transition name="modal-fade">
    <div class="modal-backdrop">
      <div class="modal"
        role="dialog"
        aria-labelledby="modalTitle"
        aria-describedby="modalDescription"
      >
        <header
          class="modal-header"
          id="modalTitle"
        >
          <slot name="header">
            This is the default title!
          </slot>
          <button
            type="button"
            class="btn-close"
            @click="close"
            aria-label="Close modal"
          >
            x
          </button>
        </header>

        <section
          class="modal-body"
          id="modalDescription"
        >
          <slot name="body">
            This is the default body!
          </slot>
        </section>

        <footer class="modal-footer">
          <slot name="footer">
            This is the default footer!
          </slot>
          <button
            type="button"
            class="btn-green"
            @click="close"
            aria-label="Close modal"
          >
            Close Modal
          </button>
        </footer>
      </div>
    </div>
  </transition>
</template>

The final version of the modal component should now resemble:

src/components/Modal.vue
<script>
  export default {
    name: 'Modal',
    methods: {
      close() {
        this.$emit('close');
      },
    },
  };
</script>

<template>
  <transition name="modal-fade">
    <div class="modal-backdrop">
      <div class="modal"
        role="dialog"
        aria-labelledby="modalTitle"
        aria-describedby="modalDescription"
      >
        <header
          class="modal-header"
          id="modalTitle"
        >
          <slot name="header">
            This is the default tile!
          </slot>
          <button
            type="button"
            class="btn-close"
            @click="close"
            aria-label="Close modal"
          >
            x
          </button>
        </header>

        <section
          class="modal-body"
          id="modalDescription"
        >
          <slot name="body">
            This is the default body!
          </slot>
        </section>

        <footer class="modal-footer">
          <slot name="footer">
            This is the default footer!
          </slot>
          <button
            type="button"
            class="btn-green"
            @click="close"
            aria-label="Close modal"
          >
            Close me!
          </button>
        </footer>
      </div>
    </div>
  </transition>
</template>

<style>
  .modal-backdrop {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    background-color: rgba(0, 0, 0, 0.3);
    display: flex;
    justify-content: center;
    align-items: center;
  }

  .modal {
    background: #FFFFFF;
    box-shadow: 2px 2px 20px 1px;
    overflow-x: auto;
    display: flex;
    flex-direction: column;
  }

  .modal-header,
  .modal-footer {
    padding: 15px;
    display: flex;
  }

  .modal-header {
    position: relative;
    border-bottom: 1px solid #eeeeee;
    color: #4AAE9B;
    justify-content: space-between;
  }

  .modal-footer {
    border-top: 1px solid #eeeeee;
    flex-direction: column;
  }

  .modal-body {
    position: relative;
    padding: 20px 10px;
  }

  .btn-close {
    position: absolute;
    top: 0;
    right: 0;
    border: none;
    font-size: 20px;
    padding: 10px;
    cursor: pointer;
    font-weight: bold;
    color: #4AAE9B;
    background: transparent;
  }

  .btn-green {
    color: white;
    background: #4AAE9B;
    border: 1px solid #4AAE9B;
    border-radius: 2px;
  }

  .modal-fade-enter,
  .modal-fade-leave-to {
    opacity: 0;
  }

  .modal-fade-enter-active,
  .modal-fade-leave-active {
    transition: opacity .5s ease;
  }
</style>

This is the basis for a modal component with accessibility and transitions.

Conclusion

In this article, you built a modal component with Vue.js.

You experimented with slots to allow your component to be reusable, transitions to create a better user experience, and ARIA attributes to make your component more accessible.

If you’d like to learn more about Vue.js, check out our Vue.js topic page for exercises and programming projects.

Creative Commons License