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
- Small - 0.8KB min/brotli
- Simple - practically no API
- Efficient - minimal DOM traversal
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.