JAVASCRIPT

Implement a Focus Trap for Modals

Enhance accessibility by implementing a focus trap in modals, ensuring keyboard navigation stays within the modal's boundaries.

document.addEventListener('DOMContentLoaded', () => {
  const openModalBtn = document.getElementById('openModal');
  const closeModalBtn = document.getElementById('closeModal');
  const modal = document.getElementById('myModal');
  const firstFocusableElement = modal.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
  const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
  let currentlyFocusedElement = null;

  function openModal() {
    modal.style.display = 'block';
    modal.setAttribute('aria-modal', 'true');
    modal.setAttribute('role', 'dialog');
    currentlyFocusedElement = document.activeElement;
    firstFocusableElement && firstFocusableElement.focus();
    document.addEventListener('keydown', handleKeyDown);
  }

  function closeModal() {
    modal.style.display = 'none';
    modal.removeAttribute('aria-modal');
    modal.removeAttribute('role');
    currentlyFocusedElement && currentlyFocusedElement.focus(); // Return focus to element that opened the modal
    document.removeEventListener('keydown', handleKeyDown);
  }

  function handleKeyDown(e) {
    if (e.key === 'Escape') {
      closeModal();
      return;
    }
    if (e.key === 'Tab') {
      e.preventDefault(); // Stop default tab behavior
      const focusable = Array.from(modal.querySelectorAll(focusableElements));
      const first = focusable[0];
      const last = focusable[focusable.length - 1];

      if (e.shiftKey) { // Shift + Tab
        if (document.activeElement === first) {
          last.focus();
        } else {
          const index = focusable.indexOf(document.activeElement);
          if (index > 0) focusable[index - 1].focus();
        }
      } else { // Tab
        if (document.activeElement === last) {
          first.focus();
        } else {
          const index = focusable.indexOf(document.activeElement);
          if (index < focusable.length - 1) focusable[index + 1].focus();
        }
      }
    }
  }

  openModalBtn.addEventListener('click', openModal);
  closeModalBtn.addEventListener('click', closeModal);

  // Close modal if clicked outside (optional, but common for modals)
  modal.addEventListener('click', (e) => {
    if (e.target === modal) {
      closeModal();
    }
  });
});
/*
HTML structure example:
<style>
  #myModal {
    display: none;
    position: fixed;
    z-index: 1001;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    overflow: auto;
    background-color: rgba(0,0,0,0.4);
    padding-top: 50px;
  }
  .modal-content {
    background-color: #fefefe;
    margin: 5% auto;
    padding: 20px;
    border: 1px solid #888;
    width: 80%;
    max-width: 500px;
  }
</style>
<button id="openModal">Open Modal</button>

<div id="myModal">
  <div class="modal-content">
    <h2>Modal Title</h2>
    <p>This is modal content. Try tabbing!</p>
    <input type="text" placeholder="Input 1">
    <button>Another Button</button>
    <a href="#">A Link</a>
    <input type="text" placeholder="Input 2">
    <button id="closeModal">Close</button>
  </div>
</div>
*/
How it works: This snippet implements an essential accessibility feature: a 'focus trap' for modals. When the modal opens, focus is programmatically set to its first focusable element. A 'keydown' listener on the document intercepts Tab key presses. If the user tabs past the last focusable element in the modal, focus is returned to the first, and vice-versa for Shift+Tab, ensuring keyboard navigation is confined within the modal. Escape key closes the modal and returns focus to the element that opened it.

Need help integrating this into your project?

Our team of expert developers can help you build your custom application from scratch.

Hire DigitalCodeLabs