Let’s Build: With JavaScript – How to Code a Modal with Vanilla JavaScript

I’m back with my next installment of the Let’s Build: With JavaScript series. In this build, learn how to code a modal with vanilla JavaScript.

This series is all about building common components used on the web today. I use a combination of HTML, CSS, and vanilla JavaScript to accomplish this.

This is both an exercise for myself and a “show and tell” of how you can accomplish things you might have once used frameworks like jQuery for with vanilla JavaScript.

This build focuses on coding an accessible modal using styles from Tailwind and our own custom JavaScript. Key features of the modal are to open and close as well as close the modal by clicking outside the main dialog window container element.

Download the Source Code

HTML

Note that I link to Tailwind via CodePen. You can use the Tailwind CDN for experiments, though it is recommended to download the whole framework for full control.

https://cdn.jsdelivr.net/npm/tailwindcss/dist/tailwind.min.css

The HTML assumes a few things about future modals you introduce in your website or app. The modal target will be a link (a button is probably a better element to use here) with an href containing the name of the id of the modal in mention. The modal target link has an href of href="modal" which is a reference we employ to the modal itself.

I also need to have a few classes on both the modal target link and the modal div itself. This lets us target both with JavaScript respectively.

<div class="container mx-auto py-10">

  <div class="flex justify-center">

    <a href="#modal" class="modal-trigger bg-teal hover:bg-teal-dark shadow text-white font-bold py-4 px-6 rounded no-underline">Trigger Modal</a>

  </div>

<div id="modal" class="modal fixed pin p-30 w-100 h-100 m-0 z-20 mt-4" role="dialog">
  <div class="modal-overlay fixed pin z-10 bg-teal-dark"></div>
  <div class="modal-container relative bg-white rounded-lg shadow max-w-lg mx-auto p-16 z-30" role="document">

    <h3 class="font-sans text-2xl mb-2 text-teal-dark">Welcome to a super modal</h3>

    <p class="font-serif leading-normal mb-4">Lorem ipsum, dolor sit amet consectetur adipisicing elit. Esse hic quam velit fugiat ipsam, quos atque ex aperiam repellendus sequi qui perspiciatis deserunt officia fugit adipisci, consectetur veritatis, explicabo sit!</p>

    <button class="bg-white hover:bg-grey-lightest text-grey-darkest font-semibold py-2 px-4 border border-grey-light rounded pin-b pin-r absolute mb-6 mr-6">Do something</button>

    <button class="modal-close absolute pin-t pin-r w-16 h-16" aria-label="Close">
      <svg width="24" height="24" viewBox="0 0 24 24">
        <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path>
</svg>
    </button>
  </div>
</div>

</div>

CSS/SCSS

The custom CSS is below. Here I defined some custom styles to handle hiding and show the modal itself with a bit of transition. All of this is possible with Tailwind minus transitions as well.

.modal {
  opacity: 0;
  visibility: hidden;
  transition: visibility 0s lineaer 0.1s, opacity 0.3s ease;

  &.open {
    visibility: visible;
    opacity: 1;
    transition-delay: 0s;
  }
}

JavaScript

The JavaScript is compromised of two functions responsible for opening and closing the modal. The openModal function listens for our modal trigger event on click. If that click is found it loops through any modals on the page and traverse them in the DOM based on the modal target href. This in return is targeted by the matching id of the modal itself. See the video for a much more in-depth explanation.

The closeModal function does what you would expect. The function is extended it a bit further to close the modal by clicking outside of the main container as well.

function openModal() {
  let modalTrigger = document.querySelectorAll('.modal-trigger');

  modalTrigger.forEach(function(trigger) {
    trigger.addEventListener('click', function(event) {

      // remove "#" from #modal
      const target = this.getAttribute('href').substr(1);

      // use dynamic target to reference given modal
      const modalWindow = document.getElementById(target);

      if(modalWindow.classList) {
        modalWindow.classList.add('open');
      }

      event.preventDefault();
    });
  });
}

function closeModal() {
  let closeBtns = document.querySelectorAll('.modal-close');
  let modalOverlays = document.querySelectorAll('.modal-overlay');

  closeBtns.forEach(function(btn) {
    btn.addEventListener('click', function(event) {

      // target the whole modal
      const modalWindow = this.parentNode.parentNode;

      modalWindow.classList.remove('open');
    });
  });

  modalOverlays.forEach(function(overlay) {
    // get the whole modal using overlay argument
    const modalWindow = overlay.parentNode;

    // close modal if click event is fired on overlay background
    overlay.addEventListener('click', function() {
      modalWindow.classList.remove('open');
    });
  });
}

function fireWhenReady(func) {
  document.addEventListener('DOMContentLoaded', func);
}

fireWhenReady(openModal);
fireWhenReady(closeModal);