Introduction

Modals are one of the most common frontend components used in web development. A modal is a component that pops up on a website and acts as a separate window. They're simple enough to implement but there's actually quite a bit that goes into a good modal.

Since modals act as a separate window, when they're active they should be the only thing on the website that can be interacted with.

A couple ways of doing this that most developers will think of include:

  1. Disabling scrolling on the rest of the website, and only allowing the modal to be scrolled (if it needs to be)
  2. Disable clicking an elements anywhere outside of the modal. This is often achieved by adding an overlay to the modal that will cover the rest of the elements on the page.

However, what if a user visiting your website relies on a keyboard and will be navigating your website using the tab button. What happens when they open the modal and start tabbing through elements in the modal? Once they reach the last element in the modal where do they go?

If they get to the last element and start tabbing through the rest of the website (outside of the modal) that's a problem. When a user has a modal open they should be confined to that modal until they decide to leave it, including when navigating the modal with a keyboard.

Trapping Focus Inside the Modal

The following code will keep all tabbing confined to the modal. Meaning when a user tabs through the modal and reaches that last focusable element, their next tab will focus on the first focusable element in the modal, rather than than the next element on the page.

trapFocus.js

export function trapFocus(e, modalId) {
  const isTabPressed = e.key === `Tab` || e.keyCode === 9;

  if (!isTabPressed) {
    return;
  }
  const focusableElements = `button, [href], input, select, textarea, iframe, [tabindex]:not([tabindex="-1"])`;
  const modal = document.getElementById(modalId);

  // get focusable elements in modal
  const firstFocusableElement = modalId === 'mobile-nav-wrapper' ? document.querySelector(`.toggleMobileNav`) : modal.querySelectorAll(focusableElements)[0];
  const focusableContent = modal.querySelectorAll(focusableElements);
  const lastFocusableElement = focusableContent[focusableContent.length - 1];

  if (e.shiftKey) {
    if (document.activeElement === firstFocusableElement) {
      lastFocusableElement.focus();
      e.preventDefault();
    }
  } else if (document.activeElement === lastFocusableElement) {
    firstFocusableElement.focus();
    e.preventDefault();
  }
}

We'll use the initTrapFocus function to call the trapFocus function above so that we can add the function as an event listener when the modal is opened and then remove the listener when the modal is closed.

initTrapFocus.js

function initTrapFocus(e) {
  return trapFocus(e, `modal-id`);
}

/**
 * Start listening for tabs by adding an event listener when the modal opens.
 */
function openModal() {
  ...
  document.addEventListener(`keydown`, initTrapFocus);
  ...
}

/**
 * Stop listening for tabs by removing the event listener when the modal closes.
 */
function closeModal() {
  ...
  document.removeEventListener(`keydown`, initTrapFocus);
  ...
}

Live Example

https://codepen.io/zpthree/full/RwdWNvM