Open-source project
theKashey/react-focus-lock avatar
theKashey/react-focus-lock

react-focus-lock: React focus containment and return behavior

It is a trap! A lock for a Focus. 🔓

1,391 stars75 forksJavaScriptMIT

At a glance

What is it?
react-focus-lock contains keyboard focus within a React subtree, supports portals and related controls, and can return focus after unmount. It fits modal and task interfaces, but does not lock scrolling or substitute for application-level focus policy.
Who is it for?
React teams building modals or constrained tasks should adopt react-focus-lock after verifying tab order, portal placement, returnFocus behavior, and shutdown handling. Projects that require scroll blocking, text-to-speech control, or a full accessibility policy should handle those concerns separately, and React versions below 16.8 are outside version 2 support.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 5 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Why react-focus-lock contains React focus

react-focus-lock addresses a focused accessibility problem: keyboard users must be able to complete a modal dialog or constrained task without tabbing into content that is visually or conceptually outside the active interaction. The library does this by watching browser focus behavior instead of reproducing the Tab key. According to the README, that design also lets it account for elements with positive tab indexes rather than assuming one fixed keyboard sequence.

The intended users are React developers implementing modal dialogs, focused tasks, and components that need to constrain user intention. A typical component wraps its active content in `FocusLock`, which moves focus inside when it mounts. The default behavior is deliberately conservative about restoration: `returnFocus` is disabled, so unmounting does not automatically restore the previously focused element.

This package handles focus containment only. The README explicitly distinguishes it from scroll locking and text-to-speech control. A dialog that also needs to prevent background scrolling or manage assistive-technology output therefore requires other code.

The guards, subtree, and focus cycle

The documented mechanism is observation rather than synthetic keyboard control. FocusLock manages an internal wrapper around the content and places focus guards around its focusable range. Those guards are described as virtual inputs that secure the tab index. When focus reaches an edge, the library moves focus back into the permitted subtree, creating the effective trap without pretending to press Tab for the user.

The wrapper defaults to a `div`, but the `as` prop can select another element. `lockProps` passes additional props to that wrapper, while `className` controls its class. Ref forwarding can expose the internal node when the caller needs a direct reference.

FocusLock works with React portals. Content rendered through a portal can remain part of the lock even when its DOM position appears outside the visible wrapper. This is significant for dialogs that render menus, confirmation panels, or other controls through portals. The lock follows the React subtree represented by those nodes rather than treating every portal as an unconditional escape boundary. `whiteList` provides another control by allowing named external locations that FocusLock should treat as allowed.

Positive tab indexes require explicit configuration through `hasPositiveIndices`. Cross-frame capture is also opt-in through `crossFrame`, which matters when focus can enter an iframe.

Groups and shards for related focus regions

React applications often need more than one locked region. react-focus-lock supports two documented ways to combine focus targets. The `group` prop names a focus group, allowing focus to be scattered across isolated locks that belong together. The README describes this as moving between separate locks rather than treating every component as an independent trap.

The `shards` prop takes an array of refs that FocusLock should consider part of its active region. This fits a dialog whose useful controls are split across several DOM locations. Shards differ from portals in implementation and intent: a portal remains connected through React rendering, while a shard is explicitly registered as part of the lock.

These controls also introduce policy decisions. A named group may make it possible to leave one component boundary, but only within the intended group. Incorrectly grouping or registering nodes can make adjacent interface areas part of one containment boundary. Conversely, failing to register a valid related node can force focus into a narrower region than the task requires. The README documents the mechanisms but does not provide a decision guide for choosing between `group`, `shards`, and `whiteList`.

A first FocusLock component in a React application

The README identifies the package on npm as `react-focus-lock`, but the README installation section does not show an exact package-manager command or a package-manager choice. The shortest documented first use is to import the default export and wrap the content that should retain focus. The reader should see an ordinary React component receive the imported `FocusLock` component and render its close action inside the wrapper.

js
import FocusLock from 'react-focus-lock';

const JailForAFocus = ({onClose}) => (
  <FocusLock>
    You can not leave this form
    <button onClick={onClose} />
  </FocusLock>
);

On activation, the documented default is `autoFocus=true`, so the library attempts to move focus inside. The button shown here is only the README example action; the project does not prescribe that every use case must contain a button.

For modal use, the focus restoration setting deserves deliberate attention. Since `returnFocus` defaults to false, a caller that wants focus sent back to the previously active control must enable it or implement that behavior. The README recommends enabling `returnFocus` for a better experience while also noting that applications can implement unmounting and focus management themselves.

Version 2 requires React 16.8 or later because the library uses hooks. The package metadata lists React 16.8, 17, 18, 19, and a React 19 release candidate as accepted peer ranges. Applications still aligned with the separately documented React 15 and 16 compatibility of version 1 cannot upgrade directly to the version 2 line.

Deferred loading through UI and sidecar entry points

The package separates its rendered component from focus-control logic. `react-focus-lock/UI` is documented as the DOM-facing component, while `react-focus-lock/sidecar` contains the focus-lock logic loaded through `use-sidecar`. The default import keeps the common path simple, but applications that do not need focus control until a dialog opens can defer that part.

The README shows `sidecar` receiving a dynamic import factory. The README example is truncated before the complete import target, so it would be unsafe to complete the expression from inference.

js
import FocusLockUI from "react-focus-lock/UI";
import {sidecar} from "use-sidecar";

const FocusLockSidecar = sidecar(
  () => import(/* webpackPrefetch: true */

This split has a measurable size rationale in the README, which lists the UI portion as 1.5kb and the sidecar as 3.5kb, versus 4kb for the default entry point. Those figures are project documentation, not an independent bundle measurement. The package metadata marks sidecar-related files as having side effects, which is consistent with the lazy-loading structure but means bundler behavior deserves review when changing the build configuration.

Imperative hooks, declarative controls, and failure modes

Focus management is not limited to the wrapper. `useFocusInside(nodeRef)` moves focus inside a given node, while `useFocusScope()` returns `autofocus`, `focusNext`, and `focusPrev`. `useFocusState()` tracks focus for a node, and `useFocusController(nodeRef)` offers lower-level scope control without FocusLock.

The declarative components have narrower jobs. `AutoFocusInside` asks autofocus logic to search inside its subtree, `MoveFocusInside` forcibly moves focus inside on mount, and `FreeFocusInside` hides internals from FocusLock for unmanaged focus. Data attributes can also control behavior indirectly. These APIs are useful when an application already owns its focus transitions, but mixing forced movement, unmanaged regions, and automatic activation can create competing instructions.

Several documented modes affect accessibility and behavior. `persistentFocus=false` requires an element to remain focused, disables text selection inside the lock, and disables outside focus. `noFocusGuards=false` removes the guards that protect the tab boundary, so disabling them changes the lock mechanism itself. Setting `disabled` changes behavior without changing the component tree.

Browser settings create another constraint. On macOS, Safari and Firefox may exclude non-controls from normal Tab traversal according to system settings. The README points to Option+Tab in Safari and Control+F7 for Firefox settings. Application code cannot make every operating-system preference conform to the lock, so QA must include the browsers and keyboard settings used by the target audience.

Release history, MIT licensing, and adoption boundaries

The repository is not archived, and its last push was on 2026-10-03. Recent release notes show v2.13.0 on 2024-08-25 with a focus-management update, v2.12.0 on 2024-04-20 with an ESM compatibility note, and v2.9.0 on 2022-05-01 with React 18 and Shadow DOM support. The package file currently declares version 2.13.7, which differs from the newest release listed in the release information. That distinction matters when an adopter pins versions or audits which source corresponds to a published release.

The project uses the MIT License. That permits use, modification, distribution, and private reuse under the license conditions, including preservation of its copyright and license notice. This is a factual summary, not legal advice. Teams still need to review the repository `LICENSE` file for their compliance process.

The principal upgrade cost is behavioral rather than conceptual. Applications must test newly rendered elements, portalled controls, positive tab indexes, iframes, and return-focus transitions. A consumer that relies on Safari or Firefox Tab behavior must also account for system settings. Because FocusLock controls only focus, adding scroll locking or broader assistive-technology policy increases the number of components involved.

Editorial conclusion

React teams building modals or constrained tasks should adopt react-focus-lock after verifying tab order, portal placement, returnFocus behavior, and shutdown handling. Projects that require scroll blocking, text-to-speech control, or a full accessibility policy should handle those concerns separately, and React versions below 16.8 are outside version 2 support.

Frequently asked questions

What React versions does react-focus-lock version 2 support?

Version 2 requires React 16.8 or later because it uses hooks. The package metadata lists peer ranges for React 16.8, 17, 18, 19, and a React 19 release candidate.

Does react-focus-lock restore focus after a modal closes?

Not by default. The `returnFocus` prop is disabled by default, but callers can enable it or implement focus restoration themselves.

Can react-focus-lock contain controls rendered in a React portal?

Yes. The README states that portal content remains part of the lock even when it appears elsewhere in the DOM.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. theKashey/react-focus-lock on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/thekashey-react-focus-lock.svg)](https://hysenlabs.com/projects/thekashey-react-focus-lock)

Was this useful?