Focus trap

·

3 min read

If you have a modal then when people are tabbing through the links/buttons/forms etc inside them then you want to be sure that they can't tab off the modal. Generally speaking your modal will hide the content behind it, so anyone using a keyboard and finding themselves somewhere they can't see will be very confused.

A focus trap sounds like it's complicated, but it's pretty simple to understand. In pseudocode you would:

  • Get an array of all of the tabbable elements in the modal

  • If the current focus is on the last element and the user presses tab without pressing shift (ie they're going forwards), set the focus to the first element

  • If the current focus is on the first element and the user presses tab and shift (ie they're going backwards), set the focus to the last element

So you're putting all of the tabbable elements into a circle: going forward from the last one takes you to the first one. And going backwards from the first one takes you to the last one.

Find all the tabbable elements

const focusableElements = modal.querySelectorAll('a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])');
if (focusableElements) {
  const firstElement = focusableElements[0];
  const lastElement = focusableElements[focusableElements.length - 1];
}

This code gets every single thing that could be tabbable - although it assumes you haven't added tabindex="0" to something that wouldn't usually be tabbable.

We don't have to define the firstElement and lastElement, but it makes it easier to read if we do.

Deal with pressing tab when last element is focused

 if (document.activeElement === lastElement && !event.shiftKey) {
   event.preventDefault();
   firstElement.focus();
 }

This is saying that if the last element has focus and the shift key wasn't pressed, then intercept the key press and don't do what you normally would. Instead, focus the first element.

Deal with pressing shift + tab when first element is focused

 if (document.activeElement === firstElement && event.shiftKey) {
   event.preventDefault();
   lastElement.focus();
 }

This is saying that if the last element has focus and the shift key was pressed, then intercept the key press and don't do what you normally would. Instead, focus the last element.

The whole code

The last two pieces are all inside the condition that says to only run this if there are any focusable elements. Even if all you have is a close button, you still need to make sure that pressing tab doesn't take you out of the modal.

// Find all the buttons
const focusableElements = modal.querySelectorAll('button');
if (focusableElements) {
  const firstElement = focusableElements[0];
  const lastElement = focusableElements[focusableElements.length - 1];

  // if the lastElement has focus and shift key wasn't pressed
  // then focus the firstElement
  if (document.activeElement === lastElement && !event.shiftKey) {
    event.preventDefault();
    firstElement.focus();
  }

  // if the firstElement has focus and shift key was pressed
  // then focus the lastElement
  if (document.activeElement === firstElement && event.shiftKey) {
    event.preventDefault();
    lastElement.focus();
  }
}