Building a Modal Component with Vue.js

Filipa Lacerda

As much as I would love to avoid them, every project always ends up having a modal component. Don’t get me wrong, it’s not that I don’t find them useful, but creating an accessible component that works across multiple devices can be tricky!

In this article you’ll learn how to create a reusable modal component using transitions and slots.

Defining the Template Structure

Let’s start by defining our template. We’ll need a div for the backdrop shade, a div to act as the modal box and some elements to define it’s structure:

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>

Notice the use of slots? We could opt to use props to provide the header, body and footer, but using slots will allow for more flexibility.

The use of slots allow us to easily reuse the same modal with different types of body contents. We may use a modal to show a simple text, but we 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 we are using named slots to allow us to use more than one slot in the same component.

When we define a named slot, anything we identify with that name will be rendered instead of the original slot - let’s think of it as a placeholder.

Like a placeholder, a slot can also have a default content that will be rendered in case we don’t provide any.

Because the content provided replaces the <slot> tag, in order to guarantee our sections have the classes we want, we 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.

components/modal.vue

<script>
  export default {
    name: 'modal',

    methods: {
      close() {
        this.$emit('close');
      },
    },
  };
</script>

<template>
  <div class="modal-backdrop">
    <div class="modal">
      <header class="modal-header">
        <slot name="header">
          This is the default tile!

          <button
            type="button"
            class="btn-close"
            @click="close"
          >
            x
          </button>
        </slot>
      </header>
      <section class="modal-body">
        <slot name="body">
          I'm the default body!
        </slot>
       </section>
       <footer class="modal-footer">
          <slot name="footer">
            I'm the default footer!

            <button
              type="button"
              class="btn-green"
              @click="close"
            >
              Close me!
          </button>
        </slot>
      </footer>
    </div>
  </div>
</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 {
    border-bottom: 1px solid #eeeeee;
    color: #4AAE9B;
    justify-content: space-between;
  }

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

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

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

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

And we’ve made a very simple version of a modal box! 🎉

Adding Transitions

Notice how the modal opens abruptly? We can make it go smother by using a transition.

Vue provides a wrapper component called transition that allow us 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, we’ll use only CSS.

When an element is added or removed, six classes are applied for the enter/leave transitions. Each of them will be prefixed with the name of the transition. In this guide you’ll find a detailed explanation of how transitions work.

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

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

<style>
 .modal-fade-enter,
  .modal-fade-leave-active {
    opacity: 0;
  }

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

Now our modal opens and closes smoothly!

Making the Modal Accessible

The only thing missing is to transform this modal into an accessible one.

We can achieve that by using the aria attributes.

Adding role="dialog" will help assistive software to identify our component has being an application dialog that is separated from the rest of the UI. Although adding dialog role is helpful it’s not sufficient to make it accessible, we need to proper label it. We can achieve that through aria-labelledby and aria-describedby attributes. We can’t forget to label our close buttons too!

The final version of our modal component should now look like:

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

            <button
              type="button"
              class="btn-close"
              @click="close"
              aria-label="Close modal"
            >
              x
            </button>
          </slot>
        </header>
        <section
          class="modal-body"
          id="modalDescription"
        >
          <slot name="body">
            I'm the default body!
          </slot>
        </section>
        <footer class="modal-footer">
          <slot name="footer">
            I'm the default footer!

            <button
              type="button"
              class="btn-green"
              @click="close"
              aria-label="Close modal"
            >
              Close me!
            </button>
          </slot>
        </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 {
    border-bottom: 1px solid #eeeeee;
    color: #4AAE9B;
    justify-content: space-between;
  }

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

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

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

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

Using the Modal Component in our App

We can now use our modal component by including it in our app. You can use this codepen to play with it!

App.vue

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

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

    <modal
      v-show="isModalVisible"
      @close="closeModal"
    />
  </div>
</template>
  Tweet It
✖ Clear

🕵 Search Results

🔎 Searching...