What is a modal? And how to build a CSS-only Modal

CSS has come a long way from its humble beginnings of basic styling. Thanks to the use of pseudo-classes, we’re now able to style elements based on certain behaviour. In this tutorial, we’ll be using the :target pseudo-class to build a modal element. 

What is a modal?

A modal is an element displayed on top of other elements on a webpage, preventing interaction with the rest of the webpage until the modal is dismissed. Modals are usually used to display information pertinent to the user or call attention to a required action.

Since modals require user interaction to be displayed or dismissed, they are commonly built using JavaScript. There’s even an inbuilt JavaScript API specifically for handling modals without needing to write any JavaScript code.

In this tutorial, however, we’ll be looking at how to build modals without the use of any JavaScript at all. Instead, we’ll build our fully functional modal CSS component.

The :target CSS pseudo-class represents a unique element (the target element) with an id matching the URL’s fragment. – MDN

1. Building the modal features

We’ll be building a modal that

  1. opens when a button is clicked and
  2. closes when the area outside the modal or the close button is clicked.

So we’ll need the following elements:

  • An element to control opening the modal
  • A modal container element to hold the modal content
  • A modal overlay to handle when outside the modal element is clicked
  • A close button to handle closing the modal element

Opening the modal

To handle opening the modal class, we’ll set the modal id in the URL fragment using an anchor element. Anchor elements can link to a certain section of a webpage by using a # and then the element id.

1
<!-- links to a different website -->
2
<a href="https://tutsplus.com/authors/jemima-abu"></a>
3

4
<!-- links to section of the same webpage with matching id -->
5
<a href="#tutorials"></a>

When the anchor tag sets a URL fragment, the page URL looks like this: https://tutsplus.com/authors/jemima-abu#tutorials

When using fragments, the window location history is changed as well. This means if a user is on https://tutsplus.com/authors/jemima-abu#tutorials and they press the back button on their browser, they will be redirected to https://tutsplus.com/authors/jemima-abu which is the same webpage.

The fragment is what the CSS :target pseudo-class uses to apply styling to the element with matching id. Once the modal id is set as the URL fragment, then we can use :target to display the modal element.

Closing the modal

By that same logic, if we change the id in the URL fragment to something else, the :target styling will no longer be applied to the modal. We’ll set our close button and modal overlay elements to be anchor tags that reset the URL fragment, removing the target styling from the modal and hiding it in the process.

To close the modal, we can either set the href of our overlay and close button anchor elements to a blank fragment e.g. href=”#”, set it to another section id, or use an id that doesn’t exist #blank.

The benefit to using a blank fragment is that the URL will not contain unnecessary text but it will cause the page to scroll to the top when the modal is closed.

The benefit to using another section id is that we can decide what element gets focus when the modal section is closed but it may cause a slight page jump as the element will be scrolled into view.

The benefit to using an id that doesn’t exist is that the page will maintain the same scroll position as when the modal is closed but the fragment text will be displayed in the URL for some browsers and may look a bit out-of-place.

HTML markup for the modal

The HTML layout for our modal looks like this:

1
<main>
2
  <a class="modal-btn" href="#modal">Click here to open modal</a>
3
</main>
4

5
<section role="dialog" class="modal" id="modal" aria-labelledby="modal-title">
6
  <a class="modal-overlay" href="#" tabindex="-1"></a>
7

8
  <div class="modal-content">
9
    <a title="Close modal" aria-label="Close modal" href="#" class="modal-close">&times; </a>
10
    <h2 id="modal-title"> Hello there! </h2>
11
  </div>
12
</section>

We’ve used the following attributes to handle accessible labelling for our modals:

  • role="dialog": This tells assistive technology that the element is a modal
  • aria-labelledby="modal-title": This provides a title for the modal container, by using the text in the ‘modal-title’ element
  • aria-label="Close modal": This provides descriptive text for the close modal button
  • tabindex="-1": This prevents the modal overlay from gaining focus when using a keyboard. This is because the overlay serves as a mainly visual cue and not a necessary function.

2. Applying styling to the modal

Once we have our layout set up, we can apply some basic styling to the anchor tags and modal elements. We want the anchor tag to look like a button and the modal element to be placed on top of other elements with a semi-transparent background for the modal overlay.

1
.modal-btn {
2
  transition: background 250ms;
3
  padding: 16px 24px;
4
  border-radius: 4px;
5
  background-color: #0f0f0f;
6
  color: #fcfcfc;
7
  text-transform: uppercase;
8
  font-size: 12px;
9
  letter-spacing: 0.1em;
10
  margin-top: 32px;
11
  display: inline-block;
12
  text-decoration: none;
13
}
14

15
.modal-btn:hover,
16
.modal-btn:focus {
17
  background-color: #0f0f0fdd;
18
}
19

20
.modal {
21
  position: fixed;
22
  min-height: 100vh;
23
  width: 100%;
24
  top: 0;
25
  left: 0;
26
  display: flex;
27
  z-index: 2;
28
}
29

30
.modal-overlay {
31
  width: 100%;
32
  height: 100%;
33
  position: absolute;
34
  background-color: rgba(0, 0, 0, 0.5);
35
  left: 0;
36
}
37

38
.modal-content {
39
  transition: transform 1s;
40
  background: #fff;
41
  width: 75%;
42
  position: relative;
43
  margin: auto;
44
  height: 75%;
45
  padding: 48px 24px;
46
  border-radius: 4px;
47
  max-width: 1000px;
48
}
49

50
.modal-close {
51
  font-size: 36px;
52
  text-decoration: none;
53
  color: inherit;
54
  position: absolute;
55
  right: 24px;
56
  top: 10px;
57
}

This is what our modal looks like so far:

We have our modal styled to be displayed by default so we just need to use the CSS :not pseudo-class to hide the modal if it’s not the target element. 

1
.modal:not(:target) {
2
  display: none;
3
}

That’s all we need to handle displaying and hiding the modal element based on which anchor tag is clicked but there are still more features to be considered.

3. Animating modal with CSS

Since we’re working with CSS, we can also apply transitions and animations to provide a smoother modal entry and exit. This is an example of the modal CSS with a fade and slide animation:

The CSS code looks like this:

1
.modal:not(:target) {
2
  visibility: hidden;
3
  transition-delay: 500ms;
4
  transition-property: visibility;
5
}
6

7
.modal:target .modal-content {
8
  transform: translateY(100vh);
9
  animation: 500ms ease-in-out slideUp forwards;
10
}
11

12
.modal:not(:target) .modal-content {
13
  transform: translateY(0);
14
  animation: 500ms ease-out slideDown forwards;
15
}
16

17
.modal:target .modal-overlay {
18
  opacity: 0;
19
  animation: 500ms linear fadeIn forwards;
20
}
21

22
@keyframes fadeOut {
23
  from {
24
    opacity: 1;
25
  }
26

27
  to {
28
    opacity: 0;
29
  }
30
}
31

32
@keyframes fadeIn {
33
  from {
34
    opacity: 0;
35
  }
36

37
  to {
38
    opacity: 1;
39
  }
40
}
41

42
@keyframes slideUp {
43
  from {
44
    transform: translateY(100vh);
45
  }
46

47
  to {
48
    transform: translateY(0);
49
  }
50
}
51

52
@keyframes slideDown {
53
  from {
54
    transform: translateY(0);
55
  }
56

57
  to {
58
    transform: translateY(100vh);
59
  }
60
}

We’ll delay the visibility transition of the modal to allow the exit animations to be completed before it’s completely hidden.

4. Modal accessibility

When building a modal element with any technology, there are some accessibility requirements that need to be put in place:

  1. The modal should gain focus once opened and shift focus back to the main page when closed.
  2. The modal content should be made visible to screen readers, only when open.
  3. The modal can be closed with the keyboard.

Thanks to the way the URL fragment works, our modal automatically gains focus when the anchor tag is clicked. This is because URL fragments scroll to the section with the matching id which causes the focus to be shifted as well.

Regarding shifting focus back to the main page, the focus will be set back on the main page if the href=”#” attribute is used on the close button and overlay.

Alternatively, we can set the focus back to a specific element, such as the modal button, by using the element id. This method is preferred as it allows the user to maintain the same focus state they were in before opening the modal.

1
<a id="modal-btn" class="modal-btn" href="#modal">Click here to open modal</a>
2

3
<section role="dialog" class="modal" id="modal" aria-labelledby="modal-title">
4
  <a class="modal-overlay" href="#modal-btnn"></a>
5

6
  <div class="modal-content">
7
    <a title="Close modal" aria-label="Close modal" href="#modal-btn" class="modal-close">&times; </a>
8
    <h2 id="modal-title"> Hello there! </h2>
9
  </div>
10
</section>

We’re going to need some JavaScript

For the rest of the features, however, we’re not able to take advantage of native behaviour so we’ll need to use some JavaScript to handle the accessibility requirements.

We’ll write a script to toggle the aria-hidden attribute for the modal and also include an event listener to allow the modal be closed when the Escape key is pressed.

Since we’re using the URL fragment to handle toggling the modal display, we can also use the same method in JavaScript with the popstate event listener.

This event listener detects when the window URL is changed. Then we can get the value of the URL fragment using the window.location.hash value. This is what our JavaScript code looks like:

1
const modal = document.getElementById('modal')
2

3
window.addEventListener('popstate', () => {
4
  if (window.location.hash === '#modal') {
5
    modal.focus();
6
    modal.setAttribute('aria-hidden', false);
7
    return
8
  }
9
  
10
  modal.setAttribute('aria-hidden', true)
11
});

We also use the keydown event listener to detect if the escape key is pressed and the modal is open. If it is, then we’ll close the modal by setting the window.location.hash value to empty

1
window.addEventListener('keydown', (e) => {
2
  if (e.key == "Escape" && window.location.hash === '#modal') {
3
    window.location.hash = ""
4
  }
5
})

This article on Accessible modal dialogs also provides more information on how to improve accessibility of modals.

Final result

And with that, we’ve built a functional modal component using CSS (and some JavaScript for accessibility).