Socket
Socket
Sign inDemoInstall

focus-trap

Package Overview
Dependencies
1
Maintainers
3
Versions
80
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

    focus-trap

Trap focus within a DOM node.


Version published
Maintainers
3
Install size
750 kB
Created

Changelog

Source

6.9.4

Patch Changes

  • f68882e: Fix docs and typings to clarify that initialFocus, fallbackFocus, and setReturnFocus options can be functions that also return selector strings.

Readme

Source

focus-trap CI license

All Contributors

Trap focus within a DOM node.

There may come a time when you find it important to trap focus within a DOM node โ€” so that when a user hits Tab or Shift+Tab or clicks around, she can't escape a certain cycle of focusable elements.

You will definitely face this challenge when you are trying to build accessible modals.

This module is a little, modular vanilla JS solution to that problem.

Use it in your higher-level components. For example, if you are using React check out focus-trap-react, a light wrapper around this library. If you are not a React user, consider creating light wrappers in your framework-of-choice.

What it does

When a focus trap is activated, this is what should happen:

  • Some element within the focus trap receives focus. By default, this will be the first element in the focus trap's tab order (as determined by tabbable). Alternately, you can specify an element that should receive this initial focus.
  • The Tab and Shift+Tab keys will cycle through the focus trap's tabbable elements but will not leave the focus trap.
  • Clicks within the focus trap behave normally; but clicks outside the focus trap are blocked.
  • The Escape key will deactivate the focus trap.

When the focus trap is deactivated, this is what should happen:

  • Focus is passed to whichever element had focus when the trap was activated (e.g. the button that opened the modal or menu).
  • Tabbing and clicking behave normally everywhere.

Check out the demos.

For more advanced usage (e.g. focus traps within focus traps), you can also pause a focus trap's behavior without deactivating it entirely, then unpause at will.

Installation

npm install focus-trap

UMD

You can also use a UMD version published to unpkg.com as dist/focus-trap.umd.js and dist/focus-trap.umd.min.js.

NOTE: The UMD build does not bundle the tabbable dependency. Therefore you will have to also include that one, and include it before focus-trap.

<head>
  <script src="https://unpkg.com/tabbable/dist/index.umd.js"></script>
  <script src="https://unpkg.com/focus-trap/dist/focus-trap.umd.js"></script>
</head>

Browser Support

IE9+

Why? Because this module uses EventTarget.addEventListener(). And its only dependency, tabbable, uses a couple of IE9+ functions.

Usage

createFocusTrap(element[, createOptions])

import * as focusTrap from 'focus-trap'; // ESM
const focusTrap = require('focus-trap'); // CJS
// UMD: `focusTrap` is defined as a global on `window`

trap = focusTrap.createFocusTrap(element[, createOptions]);

Returns a new focus trap on element (one or more "containers" of tabbable nodes that, together, form the total set of nodes that can be visited, with clicks or the tab key, within the trap).

element can be:

  • a DOM node (the focus trap itself);
  • a selector string (which will be passed to document.querySelector() to find the DOM node); or
  • an array of DOM nodes or selector strings (where the order determines where the focus will go after the last tabbable element of a DOM node/selector is reached).

A focus trap must have at least one container with at least one tabbable/focusable node in it to be considered valid. While nodes can be added/removed at runtime, with the trap adjusting to added/removed tabbable nodes, an error will be thrown if the trap ever gets into a state where it determines none of its containers have any tabbable nodes in them and the fallbackFocus option does not resolve to an alternate node where focus can go.

createOptions
  • onActivate {() => void}: A function that will be called before sending focus to the target element upon activation.
  • onPostActivate {() => void}: A function that will be called after sending focus to the target element upon activation.
  • checkCanFocusTrap {(containers: Array<HTMLElement | SVGElement>) => Promise<void>}: Animated dialogs have a small delay between when onActivate is called and when the focus trap is focusable. checkCanFocusTrap expects a promise to be returned. When that promise settles (resolves or rejects), focus will be sent to the first tabbable node (in tab order) in the focus trap (or the node configured in the initialFocus option). Due to the lack of Promise support, checkCanFocusTrap is not supported in IE unless you provide a Promise polyfill.
  • onDeactivate {() => void}: A function that will be called before returning focus to the node that had focus prior to activation (or configured with the setReturnFocus option) upon deactivation.
  • onPostDeactivate {() => void}: A function that will be called after the trap is deactivated, after onDeactivate. If the returnFocus deactivation option was set, it will be called after returning focus to the node that had focus prior to activation (or configured with the setReturnFocus option) upon deactivation; otherwise, it will be called after deactivation completes.
  • checkCanReturnFocus {(trigger: HTMLElement | SVGElement) => Promise<void>}: An animated trigger button will have a small delay between when onDeactivate is called and when the focus is able to be sent back to the trigger. checkCanReturnFocus expects a promise to be returned. When that promise settles (resolves or rejects), focus will be sent to to the node that had focus prior to the activation of the trap (or the node configured in the setReturnFocus option). Due to the lack of Promise support, checkCanReturnFocus is not supported in IE unless you provide a Promise polyfill.
  • initialFocus {HTMLElement | SVGElement | string | false | (() => HTMLElement | SVGElement | string | false)}: By default, when a focus trap is activated the first element in the focus trap's tab order will receive focus. With this option you can specify a different element to receive that initial focus. Can be a DOM node, or a selector string (which will be passed to document.querySelector() to find the DOM node), or a function that returns any of these. You can also set this option to false (or to a function that returns false) to prevent any initial focus at all when the trap activates.
    • ๐Ÿ’ฌ Setting this option to false (or a function that returns false) will prevent the fallbackFocus option from being used.
    • โš ๏ธ See warning below about Shadow DOM and selector strings.
  • fallbackFocus {HTMLElement | SVGElement | string | () => HTMLElement | SVGElement | string}: By default, an error will be thrown if the focus trap contains no elements in its tab order. With this option you can specify a fallback element to programmatically receive focus if no other tabbable elements are found. For example, you may want a popover's <div> to receive focus if the popover's content includes no tabbable elements. Make sure the fallback element has a negative tabindex so it can be programmatically focused. The option value can be a DOM node, a selector string (which will be passed to document.querySelector() to find the DOM node), or a function that returns any of these.
    • ๐Ÿ’ฌ If initialFocus is false (or a function that returns false), this function will not be called when the trap is activated, and no element will be initially focused. This function may still be called while the trap is active if things change such that there are no longer any tabbable nodes in the trap.
    • โš ๏ธ See warning below about Shadow DOM and selector strings.
  • escapeDeactivates {boolean} | (e: KeyboardEvent) => boolean): Default: true. If false or returns false, the Escape key will not trigger deactivation of the focus trap. This can be useful if you want to force the user to make a decision instead of allowing an easy way out. Note that if a function is given, it's only called if the ESC key was pressed.
  • clickOutsideDeactivates {boolean | (e: MouseEvent | TouchEvent) => boolean}: If true or returns true, a click outside the focus trap will immediately deactivate the focus trap and allow the click event to do its thing (i.e. to pass-through to the element that was clicked). This option takes precedence over allowOutsideClick when it's set to true. Default: false.
    • ๐Ÿ’ฌ If a function is provided, it will be called up to twice (but only if the click occurs outside the trap's containers): First on the mousedown (or touchstart on mobile) event and, if true was returned, again on the click event. It will get the same node each time, and it's recommended that the returned value is also the same each time. Be sure to check the event type if the double call is an issue in your code.
    • โš ๏ธ If you're using a password manager such as 1Password, where the app adds a clickable icon to all fillable fields, you should avoid using this option, and instead use the allowOutsideClick option to better control exactly when the focus trap can be deactivated. The clickable icons are usually positioned absolutely, floating on top of the fields, and therefore not part of the container the trap is managing. When using the clickOutsideDeactivates option, clicking on a field's 1Password icon will likely cause the trap to be unintentionally deactivated.
  • allowOutsideClick {boolean | (e: MouseEvent | TouchEvent) => boolean}: If set and is or returns true, a click outside the focus trap will not be prevented (letting focus temporarily escape the trap, without deactivating it), even if clickOutsideDeactivates=false. Default: false.
    • ๐Ÿ’ฌ If this is a function, it will be called up to twice on every click (but only if the click occurs outside the trap's containers): First on mousedown (or touchstart on mobile), and then on the actual click if the function returned true on the first event. Be sure to check the event type if the double call is an issue in your code.
    • ๐Ÿ’ก When clickOutsideDeactivates=true, this option is ignored (i.e. if it's a function, it will not be called).
    • Use this option to control if (and even which) clicks are allowed outside the trap in conjunction with clickOutsideDeactivates=false.
  • returnFocusOnDeactivate {boolean}: Default: true. If false, when the trap is deactivated, focus will not return to the element that had focus before activation.
  • setReturnFocus {HTMLElement | SVGElement | string | (previousActiveElement: HTMLElement | SVGElement) => HTMLElement | SVGElement | string | false}: By default, on deactivation, if returnFocusOnDeactivate=true (or if returnFocus=true in the deactivation options), focus will be returned to the element that was focused just before activation. With this option, you can specify another element to programmatically receive focus after deactivation. It can be a DOM node, a selector string (which will be passed to document.querySelector() to find the DOM node upon deactivation), or a function that returns any of these to call upon deactivation (i.e. the selector and function options are only executed at the time the trap is deactivated). Can also be false (or return false) to leave focus where it is at the time of deactivation.
    • ๐Ÿ’ฌ Using the selector or function options is a good way to return focus to a DOM node that may not exist at the time the trap is activated.
    • โš ๏ธ See warning below about Shadow DOM and selector strings.
  • preventScroll {boolean}: By default, focus() will scroll to the element if not in viewport. It can produce unintended effects like scrolling back to the top of a modal. If set to true, no scroll will happen.
  • delayInitialFocus {boolean}: Default: true. Delays the autofocus to the next execution frame when the focus trap is activated. This prevents elements within the focusable element from capturing the event that triggered the focus trap activation.
  • document {Document}: Default: window.document. Document where the focus trap will be active. This allows to use FocusTrap in an iFrame context.
  • tabbableOptions: (optional) tabbable options configurable on FocusTrap (all the common options).
Shadow DOM
Selector strings

โš ๏ธ Beware that putting a focus-trap inside an open Shadow DOM means you must either:

  • Not use selector strings for options that support these (because nodes inside Shadow DOMs, even open shadows, are not visible via document.querySelector()); OR
  • You must use the document option to configure the focus trap to use your shadow host element as its document. The downside of this option is that, while selector queries on nodes inside your trap will now work, the trap will not prevent focus from being set on nodes outside your Shadow DOM, which is the same drawback as putting a focus trap inside an iframe.
Closed shadows

If you have closed shadow roots that you would like considered for tabbable/focusable nodes, use the tabbableOptions.getShadowRoot option to provide Tabbable (used internally) with a reference to a given node's shadow root so that it can be searched for candidates.

trap.active

trap.active: boolean

True if the trap is currently active.

trap.paused

trap.paused: boolean

True if the trap is currently paused.

trap.activate()

trap.activate([activateOptions]) => FocusTrap

Activates the focus trap, adding various event listeners to the document.

If focus is already within it the trap, it remains unaffected. Otherwise, focus-trap will try to focus the following nodes, in order:

  • createOptions.initialFocus
  • The first tabbable node in the trap
  • createOptions.fallbackFocus

If none of the above exist, an error will be thrown. You cannot have a focus trap that lacks focus.

Returns the trap.

activateOptions:

These options are used to override the focus trap's default behavior for this particular activation.

  • onActivate {() => void}: Default: whatever you chose for createOptions.onActivate. null or false are the equivalent of a noop.
  • onPostActivate {() => void}: Default: whatever you chose for createOptions.onPostActivate. null or false are the equivalent of a noop.
  • checkCanFocusTrap {(containers: Array<HTMLElement | SVGElement>) => Promise<void>}: Default: whatever you chose for createOptions.checkCanFocusTrap.

trap.deactivate()

trap.deactivate([deactivateOptions]) => FocusTrap

Deactivates the focus trap.

Returns the trap.

deactivateOptions:

These options are used to override the focus trap's default behavior for this particular deactivation.

  • returnFocus {boolean}: Default: whatever you set for createOptions.returnFocusOnDeactivate. If true, then the setReturnFocus option (specified when the trap was created) is used to determine where focus will be returned.
  • onDeactivate {() => void}: Default: whatever you set for createOptions.onDeactivate. null or false are the equivalent of a noop.
  • onPostDeactivate {() => void}: Default: whatever you set for createOptions.onPostDeactivate. null or false are the equivalent of a noop.
  • checkCanReturnFocus {(trigger: HTMLElement | SVGElement) => Promise<void>}: Default: whatever you set for createOptions.checkCanReturnFocus. Not called if the returnFocus option is falsy. trigger is either the originally focused node prior to activation, or the result of the setReturnFocus configuration option.

trap.pause()

trap.pause() => FocusTrap

Pause an active focus trap's event listening without deactivating the trap.

If the focus trap has not been activated, nothing happens.

Returns the trap.

Any onDeactivate callback will not be called, and focus will not return to the element that was focused before the trap's activation. But the trap's behavior will be paused.

This is useful in various cases, one of which is when you want one focus trap within another. demo-six exemplifies how you can implement this.

trap.unpause()

trap.unpause() => FocusTrap

Unpause an active focus trap. (See pause(), above.)

Focus is forced into the trap just as described for focusTrap.activate().

If the focus trap has not been activated or has not been paused, nothing happens.

Returns the trap.

trap.updateContainerElements()

trap.updateContainerElements() => FocusTrap

Update the element(s) that are used as containers for the focus trap.

When you call the function createFocusTrap, you pass in an element (or selector), or an array of elements (or selectors) to keep the focus within. This method simply allows you to update which elements to keep the focus within.

A use case for this is found in focus-trap-react, where React ref's may not be initialized yet, but when they are you want to have them be a container element.

Returns the trap.

Examples

Read code in docs/ and see how it works.

Here's generally what happens in default.js (the "default behavior" demo):

const { createFocusTrap } = require('../../index');

const container = document.getElementById('default');

const focusTrap = createFocusTrap('#default', {
  onActivate: () => container.classList.add('is-active'),
  onDeactivate: () => container.classList.remove('is-active'),
});

document
  .getElementById('activate-default')
  .addEventListener('click', focusTrap.activate);
document
  .getElementById('deactivate-default')
  .addEventListener('click', focusTrap.deactivate);

Other details

One at a time

Only one focus trap can be listening at a time. If a second focus trap is activated the first will automatically pause. The first trap is unpaused and again traps focus when the second is deactivated.

Focus trap manages a queue of traps: if A activates; then B activates, pausing A; then C activates, pausing B; when C then deactivates, B is unpaused; and when B then deactivates, A is unpaused.

Use predictable elements for the first and last tabbable elements in your trap

The focus trap will work best if the first and last focusable elements in your trap are simple elements that all browsers treat the same, like buttons and inputs.**

Tabbing will work as expected with trickier, less predictable elements โ€” like iframes, shadow trees, audio and video elements, etc. โ€”ย as long as they are between more predictable elements (that is, if they are not the first or last tabbable element in the trap).

This limitation is ultimately rooted in browser inconsistencies and inadequacies, but it comes to focus-trap through its dependency Tabbable. You can read about more details in the Tabbable documentation.

Your trap should include a tabbable element or a focusable container

You can't have a focus trap without focus, so an error will be thrown if you try to initialize focus-trap with an element that contains no tabbable nodes.

If you find yourself in this situation, you should give you container tabindex="-1" and set it as initialFocus or fallbackFocus. A couple of demos illustrate this.

Development

Because of the nature of the functionality, involving keyboard and click and (especially) focus events, JavaScript unit tests don't make sense. After all, JSDom does not fully support focus events. Since the demo was developed to also be the test, we use Cypress to automate running through all demos in the demo page.

Help

Testing in JSDom

โš ๏ธ JSDom is not officially supported. Your mileage may vary, and tests may break from one release to the next (even a patch or minor release).

This topic is just here to help with what we know may affect your tests.

In general, a focus trap is best tested in a full browser environment such as Cypress, Playwright, or Nightwatch where a full DOM is available.

Sometimes, that's not entirely desirable, and depending on what you're testing, you may be able to get away with using JSDom (e.g. via Jest), but you'll have to configure your traps using the tabbableOptions.displayCheck: 'none' option.

See Testing tabbable in JSDom for more details.

Contributing

See CONTRIBUTING.

Contributors

In alphabetical order:


Anders Thorsen

๐Ÿ›

Benjamin Parish

๐Ÿ›

Clint Goodman

๐Ÿ’ป ๐Ÿ“– ๐Ÿ’ก โš ๏ธ

Daniel Tonon

๐Ÿ“– ๐Ÿ”ง ๏ธ๏ธ๏ธ๏ธโ™ฟ๏ธ ๐Ÿ’ป

DaviDevMod

๐Ÿ“–

David Clark

๐Ÿ’ป ๐Ÿ› ๐Ÿš‡ โš ๏ธ ๐Ÿ“– ๐Ÿšง

Dependabot

๐Ÿšง

Michael Reynolds

๐Ÿ›

Nate Liu

โš ๏ธ

Piotr Panek

๐Ÿ› ๐Ÿ“– ๐Ÿ’ป โš ๏ธ

Randy Puro

๐Ÿ›

Sadick

๐Ÿ’ป โš ๏ธ ๐Ÿ“–

Scott Blinch

๐Ÿ“–

Sean McPherson

๐Ÿ’ป ๐Ÿ“–

Sebastian Kriems

๐Ÿ›

Slapbox

๐Ÿ›

Stefan Cameron

๐Ÿ’ป ๐Ÿ› ๐Ÿš‡ โš ๏ธ ๐Ÿ“– ๐Ÿšง

Tyler Hawkins

๐Ÿ”ง โš ๏ธ ๐Ÿ“–

Wandrille Verlut

๐Ÿ’ป โš ๏ธ ๐Ÿ“– ๐Ÿ”ง

Will Mruzek

๐Ÿ’ป ๐Ÿ“– ๐Ÿ’ก โš ๏ธ ๐Ÿ’ฌ

Zioth

๐Ÿค” ๐Ÿ›

jpveooys

๐Ÿ›

Keywords

FAQs

Last updated on 09 Jun 2022

Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with โšก๏ธ by Socket Inc