DEV Community

Carlos Viteri
Carlos Viteri

Posted on • Originally published at c4rlosviteri.dev

Accessible components: Modals

Modal popups are frequently used on the web. Especially to confirm actions, display ads, handle forms, etc.

However, are you building accessible modals? Can you open and close them by using the keyboard only? Are they understandable for assistive technologies?

If not, no worries, let's build one from scratch.

HTML

<button class="button">Delete item?</button>

<div aria-hidden="true" class="modal">
  <div class="modal__backdrop" tabindex="-1">
    <div aria-labelledby="modal-title" aria-modal="true" class="modal__container" role="dialog">
      <header class="modal__header">
        <h2 id="modal-title">Confirm?</h2>
        <button aria-label="Close modal">×</button>
      </header>
      <div class="modal__content">
        <p>This is the description of an accessible modal</p>
      </div>
      <footer class="modal__footer">
        <button>Yes</button>
        <button aria-label="Close modal">No</button>
      </footer>
    </div>
  </div>
</div>

In the snippet above, basically we have two nodes: the trigger button and the modal.

aria and role attributes explanation:

aria-hidden="true" this attribute removes its content from the accessibility tree. We must toggle this attribute to "false" once we open the modal.

aria-labelledby="id" will tell assistive technologies that content of the id, in this case the heading level 2, is what describes the dialog.

aria-modal="true" informs assistive technologies that content outside that element is inert.

role="dialog" helps assistive technology identify the dialog's content as being grouped and separated from the rest of the page content. However, this attribute alone is not enough, it must be properly labeled and must manage the keyboard focus correctly.

aria-label is used to define a string that labels the current element. In the buttons above we are using this attribute to explain better their purposes, it is necessary only when the text inside the button is not explanatory enough.

The first button has no aria attribute because the text inside is sufficient to explain the purpose of the button.

CSS

.modal[aria-hidden="true"] {
  display: none;
}

.modal__backdrop {
  align-items: center;
  background-color: rgba(0, 0, 0, 0.6);
  bottom: 0;
  display: flex;
  justify-content: center;
  left: 0;
  position: fixed;
  right: 0;
  top: 0;
}

.modal__container {
  background-color: white;
  max-height: 100vh;
  max-width: 400px;
  overflow-y: auto;
  padding: 20px;
}

.modal__header {
  align-items: center;
  display: flex;
  justify-content: space-between;
}

First of all, we are hiding the modal if it has the attribute aria-hidden="true". Then we are creating a backdrop to set up enough contrast between the modal and the rest of the page. And finally we are preventing the vertical content to get hide of the viewport by adding a Y axis scroll if the content is higher than 100vh.

JavaScript

const focusableElements = [
    '[contenteditable]',
    '[tabindex]:not([tabindex^="-"])',
    'a[href]',
    'area[href]',
    'button:not([disabled]):not([aria-hidden])',
    'embed',
    'iframe',
    'input:not([disabled]):not([type="hidden"]):not([aria-hidden])',
    'object',
    'select:not([disabled]):not([aria-hidden])',
    'textarea:not([disabled]):not([aria-hidden])'
  ];

const modal = document.querySelector('.modal');
const openButton = document.querySelector('.button');
const closeButtons = document.querySelectorAll('.modal__close-button');

document.addEventListener('keydown', handleKeydown);
openButton.addEventListener('click', openModal);
closeButtons.forEach(button =>
    button.addEventListener('click', closeModal)
);

function handleKeydown(e) {
  const esc = 27;

  if (e.keyCode === esc) {
    closeModal();
  }
}

function openModal() {
  const focusableModalElements = modal.querySelectorAll(focusableElements);

  modal.setAttribute('aria-hidden', 'false');

  if (focusableModalElements.length) {
   focusableModalElements[0].focus();
  }
}

function closeModal() {
  modal.setAttribute('aria-hidden', 'true');
}

In the JavaScript part we will toggle the aria-hidden attribute depending on what we want to do. When opening the modal we must focus the first focusable element within the modal. And obviously we should support the esc key to close the modal.

Working example:

https://codesandbox.io/s/accessible-modal-b97jf

Top comments (0)