How to Build a JavaScript Popup Modal From Scratch
In this tutorial we’ll learn how to build JavaScript popup modals (popup windows) without using a framework like Bootstrap, or a third party library. We’ll build the whole thing from scratch, giving us complete control over how it works and looks.
Here’s the demo we’ll be creating:
1. Begin With the Page Markup
First we’ll create a modal. To do this, we’ll add the .modal
class and a unique ID to a container. Next we’ll specify the dialog by setting a .modal-dialog
element as the direct child of the .modal
. The dialog will hold the modal content. This can be any kind of content like text, images, lightboxes, user notifications/alerts, etc.
“A pop-up (or modal) is a small UI element that will appear in the foreground of a website, usually triggered as a prompt for the user to do something” – Adi Purdila
To open a modal, we’ll need any element with the data-open
attribute (normally a button
). The value of this attribute should be the ID of the desired modal.
By default, a modal will close if we click outside its boundaries or when the Esc
key is pressed. But we can also close it if we click on any element with the data-close
attribute (normally a button
).
Initially the modal will appear/disappear with a fade effect. But we have the ability to adjust the animation effect of the dialog via the data-animation
attribute. The value of this attribute which has to be added to the .modal
can be any of the following values:
slideInOutDown
slideInOutTop
slideInOutLeft
slideInOutRight
zoomInOut
rotateInOutDown
mixInAnimations
We’ll have a closer look at these values in an upcoming section.
For now, let’s get familiar with the markup needed for representing a single modal:
2. Define Some Basic Styles
With the markup ready, we’ll set up a few CSS variables and reset styles:
:root { --lightgray: #efefef; --blue: steelblue; --white: #fff; --black: rgba(0, 0, 0, 0.8); --bounceEasing: cubic-bezier(0.51, 0.92, 0.24, 1.15); } * { padding: 0; margin: 0; } button { cursor: pointer; background: transparent; border: none; outline: none; font-size: inherit; }
Next, we’ll horizontally and vertically center the page contents. Plus, we’ll give some styles to the button responsible for opening the modal:
/*CUSTOM VARIABLES HERE*/ body { display: flex; align-items: center; justify-content: center; height: 100vh; font: 16px/1.5 sans-serif; } .btn-group { text-align: center; } .open-modal { font-weight: bold; background: var(--blue); color: var(--white); padding: .75rem 1.75rem; margin-bottom: 1rem; border-radius: 5px; }
At this point we’ll focus our attention on the modal styles.
Each modal will have the following characteristics:
- It’ll be full-screen with a fixed position. That said, it will look like an overlay that covers the entire window’s width and height.
- It’ll have a dark background color.
- It’ll be hidden by default.
- The dialog will be horizontally and vertically centered.
/*CUSTOM VARIABLES HERE*/ .modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; display: flex; align-items: center; justify-content: center; padding: 1rem; background: var(--black); cursor: pointer; visibility: hidden; opacity: 0; transition: all 0.35s ease-in; }
The dialog will have a maximum width and a maximum height. Its height will be 80% of the window height. In cases where its height exceeds that value, a vertical scrollbar will appear:
.modal-dialog { position: relative; max-width: 800px; max-height: 80vh; border-radius: 5px; background: var(--white); overflow: auto; cursor: default; }
As a last thing, we’ll define a few straightforward styles for the individual content sections:
/*CUSTOM VARIABLES HERE*/ .modal-dialog > * { padding: 1rem; } .modal-header, .modal-footer { background: var(--lightgray); } .modal-header { display: flex; align-items: center; justify-content: space-between; } .modal-header .modal-close { font-size: 1.5rem; } .modal p + p { margin-top: 1rem; }
3. Toggle Modal
A page can have more than one modal. But as already discussed earlier, all modals will initially be hidden.
Open Modal
Similarly, a page can have more than one open triggers (elements with the data-open
attribute). Each time a trigger is clicked, the associated modal should become visible with a fade-in animation. Remember the data-open
attribute value has to match the ID of a modal.
Here’s the script which reveals a modal:
const openEls = document.querySelectorAll("[data-open]"); const isVisible = "is-visible"; for(const el of openEls) { el.addEventListener("click", function() { const modalId = this.dataset.open; document.getElementById(modalId).classList.add(isVisible); }); }
And the relevant CSS classes:
.modal { visibility: hidden; opacity: 0; transition: all 0.35s ease-in; } .modal.is-visible { visibility: visible; opacity: 1; }
Close Modal
With our implementation only a single modal can appear at a time (this code doesn’t support nested modals). As mentioned in the markup section above, there are three methods available for hiding it with a fade-out effect.
Let’s recap.
Firstly by clicking on the custom [data-close]
element which is located inside the modal:
const closeEls = document.querySelectorAll("[data-close]"); const isVisible = "is-visible"; for (const el of closeEls) { el.addEventListener("click", function() { this.parentElement.parentElement.parentElement.classList.remove(isVisible); }); }
Secondly by clicking on everything outside of the modal:
const isVisible = "is-visible"; document.addEventListener("click", e => { if (e.target == document.querySelector(".modal.is-visible")) { document.querySelector(".modal.is-visible").classList.remove(isVisible); } });
In this case the modal (overlay) behaves as a giant close button. For this reason we gave it cursor: pointer
.
Lastly by pressing the Esc
key:
const isVisible = "is-visible"; document.addEventListener("keyup", e => { if (e.key == "Escape" && document.querySelector(".modal.is-visible")) { document.querySelector(".modal.is-visible").classList.remove(isVisible); } });
Now is a good time to look at what we’ve created so far:
The modal looks pretty good! Notice that each time we click on an open trigger, only the corresponding modal loads.
Let’s take it one step further and examine some ideas for animating its dialog.
4. Add Dialog Animations
Like we said earlier, the default behavior of the modal is to fade-in and fade-out. But there’s the option to adjust the animation effect of the popup.
I’ve already created a bunch of animation effects which you can use as an alternative to the fade effect. To do this, just pass the data-animation="yourDesiredAnimation"
attribute to the .modal
.
For example, if you want the dialog to appear with a slide animation from left to right, you’ll need the slideInOutLeft
effect.
Behind the scenes, there are two rules which accomplish this desired animation:
/*CUSTOM VARIABLES HERE*/ [data-animation="slideInOutLeft"] .modal-dialog { opacity: 0; transform: translateX(-100%); transition: all 0.5s var(--bounceEasing); } [data-animation="slideInOutLeft"].is-visible .modal-dialog { opacity: 1; transform: none; transition-delay: 0.2s; }
Check the modal with this type of animation here:
You can check the rest of the animations by looking at the CSS tab of the final demo project. Depending on the complexity of the animations, I’ve used either CSS transitions or animations to build them.
I’ve also made use of the cubic-bezier()
function for setting the timing function for all transitions. If you don’t like the bounce effect that produces, feel free to change it to something smoother via the --bounceEasing
CSS variable.
Have a look at the final demo with all the animation effects here:
Conclusion
That’s it, folks! During this tutorial we learned how to build custom animated modal dialogs without relying on any front-end framework.
I hope you enjoyed the final result and building it helped refresh your front-end skills.
Bear in mind that we haven’t considered accessibility, so if you want to enhance this demo that certainly could be the next step.