Modals, modals, modals!

2020 March 08

CSSHTMLJavaScripta11y

What are modals?

Modals are these seemingly floating things that magically just take over the screen triggered by a certain action. How does that happen? What does it look like on mobile? Is it good for mobile? When is it appropriate to use? Can you animate it?

What considerations to think about when making one?

There are a some things to think about when it comes to modals. When are they a good option? They can be used any time you want the users attention focused on one thing.

  • Showing a lightbox for images
  • Display a dialog box confirming that the user really wants to cancel their flight
  • Save their changes to a document
  • Call to actions to sign up for newsletters and similar

Those are just a few examples.

Other things that I was wondering about when going into this was mobile. How to handle it there? Just use a smaller box or get rid of it or what?

As it turns out there is a brilliant solution to that which I from now on I will encourage on the w3.org’s own accessibility site on the subject of dialogs and modals. They suggest that on mobile to turn it into a page that acts like any other. So no box in middle with an overlay but a full site instead. This makes it look less like a modal and more like a new page on mobile which is a nicer experience.

See the pen below.

Lets build one!

HTML

What you need is something to trigger the modal to show up. In our case it will be a simple button.

<main>
  <button class="modal_show" type="button">Show Modal</button>
</main>

Then you need an overlay section that acts like a background to focus the attention on the content.

And of course you need a dialog box to hold the content. Lastly you will want a way to close the modal in the form of a button. Don’t worry it will not be the only way to close it.

Let’s add a header and a few inputs so there is some content in the modal.

<div class="modal_overlay">
  <div
    class="modal_content"
    role="dialog"
    aria-labelledby="modal_heading"
    aria-modal="true"
  >
    <h2 id="modal_heading" class="modal_heading">Modal heading</h2>
    <div class="modal_form">
      <div class="modal_form_item">
        <label for="first_name">
          <span class="label_text">First Name:</span>
        </label>
        <input
          id="first_name"
          type="text"
          aria-describedby="first_name_desc"
        />
      </div>
      <div class="modal_form_item">
        <label for="last_name">
          <span class="label_text">Last Name:</span>
        </label>
        <input
          id="last_name"
          type="text"
          aria-describedby="last_name_desc"
        />
      </div>
    </div>
    <button type="button" class="modal_close">Close</button>
  </div>
</p>

We also want to do is add some attributes to the modal content.

role='dialog'
aria-labelledby="modal_heading"
aria-modal="true"

This adds accessibility to the modal and tells a screen reader that whatever is in the heading is what this modal is about as well as telling the screen reader that it is a modal. Aria-labelledby points to the id of modal_heading and will use that to label it.

That is all the HTML we need for a modal component.

JavaScript

First we get references to the elements we need.

const showButton = document.querySelector('.modal_show');
const closeButton = document.querySelector('.modal_close');
const modal = document.querySelector('.modal_overlay');

Now we need a function that opens and closes the modal when we take certain actions.

function toggleModal() {
  modal.classList.toggle('active')
}

This adds or removes the CSS class active based on what state it is in at the moment.

Then we need to make listeners for opening and closing

showButton.addEventListener('click', toggleModal);
closeButton.addEventListener('click', toggleModal)

We also want to be able to close the modal in other ways than the close button remember? For instance through pressing the Escape key.

So we add another listener.

document.addEventListener('keyup', e => {
  if (e.keyCode === 27 && document.querySelector('.modal_overlay.active')) {
    modal.classList.toggle('active');
  }
});

This one is tied to the document and not a button that we click.

Another way we might want to close it is if we click outside the modal. If we click on the overlay so to speak.

document.addEventListener('click', e => {
  if (e.target === document.querySelector('.modal_overlay.active')) {
    modal.classList.toggle('active');
  }
});

We check if where we have clicked is on the overlay. Then we toggle the active class on the modal and since it can only be clicked when the overlay is there it will close the modal.

Now that we can open and close it we can tie it all together with some CSS to make it work.

CSS

First we need to make the overlay look like what it should. We do this by setting the overlay and targeting the role=dialog attribute.

[role='dialog'] {
  background-color: #fff;
  min-height: 100vh;
  padding: 2rem;
}

.modal_overlay {
  position: fixed;
  overflow-y: auto;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.6s ease;
}

@media screen and (min-width: 640px) {
  .modal_overlay {
    background: rgba(0, 0, 0, 0.3);
  }
  [role='dialog'] {
    position: absolute;
    top: 20vh;
    left: 50vw;
    transform: translateX(-50%);
    min-width: calc(640px - (15px * 2));
    min-height: auto;
    box-shadow: 0 19px 38px rgba(0, 0, 0, 0.12), 0 15px 12px rgba(0, 0, 0, 0.22);
  }
}

The media query is to position and set the size of the box on larger screens so it becomes a box and doesn’t just cover the screen when active.

On smaller screens it looks weird if there was a small box with dark overlay on an already small screen. Since it should be the focus on the screen it might as well be the only thing on the screen.

If we want it to show and close when we take certain actions we need to add a class called active that we can toggle.

The opacity property is for the transition so it fades in.

.active {
  visibility: visible;
  opacity: 1;
}

That is all we really need for it to work, but we are going to add some other base styles simply for visual purposes. To make it look better.

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

main {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}

button {
  background: #63b3ed;
  border: none;
  border-radius: 5px;
  padding: 0.5rem 1rem;
  font-size: 1rem;
}

button:focus {
  background: #2b6cb0;
  color: white;
}

h2 {
  margin-bottom: 1.5rem;
}

input {
  display: block;
  width: 100%;
  margin-bottom: 1.5rem;
}

.modal_close {
  margin-top: 2rem;
}

The full code and example can be seen on CodePen

Extras

Can use dom-focus-lock package to help with “trapping” the focus inside the dialog unless you want to do that manually on your own. You will need to use JavaScript to do that.

There are also versions of it for VueJS and ReactJS at the bottom of the npm link.

What I learned from this is that there is more to it than just a box showing up magically. Especially if we want this to be accessible to as many people as possible.

And there you have it. I hope you enjoyed it.

If there is anything you think that is missing and that should be added please let me know, so I can update it.

Good resources on this:

W3.org Dialog section