tab-guard

A tiny focus trap component

Tab guard is a custom element/web component that traps tab presses. It offers a declarative API via HTML, making it easy to understand and use. It can be used with all frameworks and libraries.

Goals

Installation

<script type="module" src="https://unpkg.com/tab-guard"></script>

Alternatively, you can install the package via npm or your preferred package manager:

npm install tab-guard

Then import the components into your JavaScript or TypeScript file. For example, using ES modules:

import "tab-guard";

Guide

Assuming you have followed the installation instructions, the component is now ready to use.

Basics

Traps are enabled by default. Once you tab in, you can't tab out.

<tab-guard>
  <label>Example: <input type="text" /></label>
  <label>Example: <input type="text" /></label> <button>trap</button>
</tab-guard>

You can disable a trap at any time with the disabled attribute/property

<tab-guard disabled>
  <label>Example: <input type="text" /></label>
  <label>Example: <input type="text" /></label> <button>trap</button>
</tab-guard>

Shadow DOM

Elements in shadow roots are handled correctly

<tab-guard>
  <label>Example: <input type="text" /></label>
  <div>
    <template shadowrootmode="open">
      <label>In shadow root: <input type="text" /></label>
    </template>
  </div>
  <label>Example: <input type="text" /></label> <button>trap</button>
</tab-guard>

Traps can be placed inside shadow roots, and combined with slots

<div>
  <template shadowrootmode="open">
    <tab-guard>
      <slot></slot>
      <button
        onclick="t = this.closest('tab-guard'); t.disabled = !t.disabled"
      >
        Toggle trap
      </button>
    </tab-guard>
  </template>
  <label>Example: <input type="text" /></label>
  <label>Example: <input type="text" /></label>
</div>

Nesting

Traps can be nested inside one another arbitrarily. Each has their own enabled/disabled state

<tab-guard>
  <label>Example: <input type="text" /></label>
  <label>Example: <input type="text" /></label>
  <tab-guard disabled>
    <label>Example: <input type="text" /></label>
    <label>Example: <input type="text" /></label>
    <button>trap</button>
  </tab-guard>
  <button>trap</button>
</tab-guard>

Radios

Radios are complicated since they effectively have a roving tab index. Here they are handled correctly

<tab-guard>
  <label><input name="test" type="radio" /> Radio 1</label>
  <label><input name="test" type="radio" disabled /> Radio 2</label>
  <label><input name="test" type="radio" checked /> Radio 3</label>
  <button>trap</button>
</tab-guard>

Extensibility

Tab guard is a custom element, so it can be extended like any other custom element. This allows you to add custom behavior or styling.

If you wish to tweak the logic for what is considered tabbable, you can override the isTabbable method:

import { TabGuard } from "tab-guard";

class MyTabGuard extends TabGuard {
  isTabbable(element) {
    return (
      super.isTabbable(element) &&
      someCustomCheck(element)
    );
  }
}

customElements.define("my-tab-guard", MyTabGuard);

What tab guard doesn't do

In order to remain small and efficient, tab guard does not aim for perfection but rather to be good enough. It aims to do as little as possible whilst still being useful in the general case.

Forcing focus

Some focus trap libraries forcibly move focus to the trap. Either when the trap is enabled, or on click outside. Tab guard does neither of these.

Tab guard is enabled by default so it doesn't make sense to forcibly move focus there. Instead, call focus() on the trap instance whenever you need e.g. on modal open. This will move focus to the first tabbable element.

Click outside is often a signal from the user they wish to escape a trap. So you should consider whether you really want this behavior. If you do, you can call focus() on the trap instance when you detect a click outside.

Audio and video elements

Audio and video elements are tricky because they contain multiple tabbable elements that are otherwise inaccessible to the outside world. Tab guard does not perfectly handle these right now. This will hopefully be resolved in future.

Accessibility

Focus traps are useful for keyboard accessibility e.g. they can be used to trap focus within a modal. This is useful because it prevents users from tabbing out of the modal and getting lost in the rest of the page. Care must be taken to ensure users have a way to escape from a trap.

For screen reader support, you need to add aria-hidden="true" or inert to all other elements in the page. This may be added as a built-in feature in future (PRs welcome!).

Issues

If you have an issue or feature request, please open an issue in the repository.