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.
To complete this tutorial, you will need:
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.
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:
<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:
<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:
<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:
<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
:
<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:
<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.
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:
<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-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.
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.
<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:
<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.
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.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
I have used this code, but then when I render slot in a parent component, it does not appear. It renders the default one.
This is for the main slot component, with component name “scan-message”.
the slot in the parent component
Please, what could I be doing wrong? Thank you
The way this is set up right now, the opacity is the same for both the modal backdrop and the body. Is there a way to set them separately so we can have the transparent back drop while the actual modal popup is full opaque?
I’ve successfully completed the tutorial, however, I am brand new to Vue.js. Is this modal global?
How do I re-use this from another component? For example, if i have /component/address.vue how can I implement the modal in that component?
Hey thanks for the exemple.
Someone knows how to use slot to change modal content in the App.vue?
Well explained! Thank you!