Getting keyboard-focusable elements

Published:

If you create JavaScript widgets, one of the key parts to accessibility is managing focus.

To manage focus, you need to find keyboard-focusable elements.

When you know the contents

It’s easy to find keyboard-focusable elements if you know the contents of the element beforehand.

For example, I know the focusable elements in this modal are <input> and <button>.

Modal with two inputs and one button.

I can get the focusable elements with querySelectorAll.

const focusableElements = [...modal.querySelectorAll('input, button')]

When you don’t know the contents

It’s harder to find keyboard-focusable elements if you don’t know the content beforehand.

After some research, I realised you could only focus on these elements with a keyboard:

  1. <a> with the href attribute
  2. <button>
  3. <input>
  4. <textarea>
  5. <select>
  6. <details>
  7. Elements with tabindex set to 0
  8. Elements with tabindex set to a positive number

We can get all keyboard-focusable elements with the following querySelectorAll. It looks a little complicated, but there’s no other way to include everything:

const keyboardfocusableElements = document.querySelectorAll(
  'a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])',
)

Some elements (like button) can be disabled. Disabled elements are not focusable.

In some libraries form fields are hidden visually (CSS) and for At (aria-hidden="true") and replaced by better looking components that should be accessible.

We can remove these elements with filter.

const keyboardfocusableElements = [
  ...document.querySelectorAll(
    'a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])',
  ),
].filter(el => !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden'))

Turning it into a function

This querySelectorAll code is hard to read. We can put the entire thing into a function to make it more understandable.

/**
 * Gets keyboard-focusable elements within a specified element
 * @param {HTMLElement} [element=document] element
 * @returns {Array}
 */
function getKeyboardFocusableElements(element = document) {
  return [
    ...element.querySelectorAll(
      'a[href], button, input, textarea, select, details,[tabindex]:not([tabindex="-1"])',
    ),
  ].filter(
    el => !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden'),
  )
}

Update: Lauren contacted me and asked about display:none. Certainly these elements won’t be focusable so we have to filter them out too. I also included a list of other elements that I never thought about before — embed, object, iframe, etc.

I updated the code in my Github repository — you can get the updated code if you sign up below.

Here's how to become great at JavaScript in less than 2 months

If you’re stuck because of your lack of JavaScript skills, you can stop worrying now.

We’ll help you become amazing at JavaScript — we have a course that can help you understand and use JavaScript easily.

And we’re going to give you three chapters for free because we love to help developers become superheroes.

Just click the button below to become an amazing JavaScript developer.