mrkeyoor.com_
Sat 08 Aug 21:57 UTC
npmWeb Frontendupdated 08 Aug 2026

react-focus-lock

react-focus-lock keeps browser focus inside a React subtree while that lock is active. It moves focus in on activation, observes real focus changes instead of simulating Tab, cycles through focusable elements, supports portals through groups or shards, can restore the previous target, and exposes declarative components plus hooks for programmatic movement. It is the focus-management slice of a modal, drawer, menu, or focused task. It does not add a dialog role, accessible label, Escape handling, backdrop, scroll lock, screen-reader isolation, animation, or portal container.

Verdict

react-focus-lock is a mature choice for the narrow job of containing and restoring focus, especially across portals. Install a complete dialog primitive instead when you also need semantics, outside-content isolation, scrolling behavior, and dismissal.

API stability4/5The version 2 component contract has remained recognizable across React hook-era releases and supports React 16.8, 17, 18, and 19. Core props such as disabled, returnFocus, autoFocus, group, shards, as, and lockProps are typed, while older focusOptions and allowTextSelection props are explicitly deprecated rather than silently removed.
Docs4/5The README covers default behavior, return timing, portals, groups, shards, guards, sidecar splitting, initial focus, custom wrappers, focus fighting, Safari keyboard settings, and the larger modal-accessibility picture. It is detailed but sprawling, and a few examples and size claims are older than the current hook and type surface.
Maintenance5/5Version 2.13.7 was published in November 2025, the repository was pushed in July 2026, and it is not archived. The current peer range includes React 19, the issue endpoint shows no open user issues and eight open pull requests, and the underlying focus-lock family continues to serve React, Vue, and vanilla DOM consumers.
Ecosystem5/5The package records 3,424,012 weekly downloads and 1,388 GitHub stars, and the README names adoption by Atlassian AtlasKit, Reach UI, and Storybook. It integrates with portals, old and current React majors, TypeScript, code splitting, and companion packages for scroll locking and aria hiding.

Use it if

  • You are building a custom modal, drawer, popover, or editor surface and only the focus-containment piece is missing
  • Your focusable content crosses React portals and you need explicit group or shard support
  • You need React 16.8 through 19 compatibility from one focus utility
  • You need programmatic next, previous, first, last, or autofocus control within a focus scope
Skip it if

Setup reality

Install react-focus-lock and make sure the application supplies its React peer. Version 2.13.7 accepts React 16.8 through 19; @types/react is an optional peer and declarations ship with the package. There is no CSS, provider, native build, or configuration file. Wrapping content activates the lock and moves focus to the first eligible element by default. For a modal, add role=dialog, aria-modal, an accessible label, close behavior, a backdrop, scroll locking, and screen-reader isolation yourself. Set returnFocus because it is off by default, and keep the opener mounted long enough to receive focus. Restoration after unmount or deactivation is deferred with a zero-timeout, so assertions and manual focus changes must account for that timing. Initial focus can use data-autofocus or AutoFocusInside. Portaled controls need a shared group or refs in shards if they should participate in tab order; shards have no surrounding guards, so edge layouts may need InFocusGuard. The default wrapper is a div, but as and lockProps can change it and add attributes. Positive tabIndex values require hasPositiveIndices, while persistentFocus is intentionally aggressive and can prevent text selection. The type definitions mark focusOptions and allowTextSelection deprecated. Avoid multiple installed copies or nested traps from different libraries, because competing managers can repeatedly pull focus away from each other. Test keyboard traversal in real browsers, including Safari, where operating-system settings affect which elements Tab reaches; jsdom alone does not reproduce the browser focus model.

Patterns

Contain focus in a custom modallock-modal-focus

import FocusLock from 'react-focus-lock';

function Dialog({ onClose }) {
  return (
    <div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
      <FocusLock returnFocus>
        <h2 id="dialog-title">Edit profile</h2>
        <input aria-label="Display name" />
        <button onClick={onClose}>Close</button>
      </FocusLock>
    </div>
  );
}

FocusLock handles focus only; add Escape handling, outside-content isolation, scroll locking, and any backdrop separately.

Choose the initial focus targetchoose-initial-focus

<FocusLock returnFocus>
  <input aria-label="Search" data-autofocus />
  <button>Apply</button>
  <button>Cancel</button>
</FocusLock>

If several elements have data-autofocus, the first eligible target wins when the lock activates.

Mark an autofocus region declarativelyautofocus-component

import FocusLock, { AutoFocusInside } from 'react-focus-lock';

<FocusLock>
  <button>Before</button>
  <AutoFocusInside>
    <input aria-label="Project name" />
  </AutoFocusInside>
</FocusLock>

AutoFocusInside only affects lock activation and does nothing when rendered outside a FocusLock.

Disable a lock without unmounting its contenttoggle-focus-lock

<FocusLock
  disabled={!open}
  returnFocus
  onActivation={() => setActive(true)}
  onDeactivation={() => setActive(false)}
>
  <Panel hidden={!open} />
</FocusLock>

Focus restoration after disabling is asynchronous; wait for the next task before asserting document.activeElement.

Include portaled controls with a shardinclude-portal-shard

import { createPortal } from 'react-dom';
import { useRef } from 'react';
import FocusLock from 'react-focus-lock';

function Editor() {
  const toolbarRef = useRef(null);
  return (
    <>
      <FocusLock shards={[toolbarRef]}>
        <textarea aria-label="Document" />
      </FocusLock>
      {createPortal(
        <div ref={toolbarRef}><button>Bold</button></div>,
        document.body,
      )}
    </>
  );
}

A shard participates in the lock but does not receive automatic focus guards around its own DOM position.

Join main and portaled lock regions by groupgroup-scattered-locks

const group = 'command-palette';

<FocusLock group={group}>
  <input aria-label="Command" />
</FocusLock>

{createPortal(
  <FocusLock group={group} disabled>
    <button>Search files</button>
  </FocusLock>,
  document.body,
)}

Only one lock in the group should be active; the disabled lock marks its portaled subtree as part of the scattered focus area.

Exclude a subtree from focus enforcementallow-unmanaged-focus

import FocusLock, { FreeFocusInside } from 'react-focus-lock';

<FocusLock>
  <button>Locked action</button>
  <FreeFocusInside>
    <div id="third-party-modal-root" />
  </FreeFocusInside>
</FocusLock>

Use this when another focus manager owns the excluded subtree and would otherwise fight with react-focus-lock.

Put dialog attributes on the lock wrappercustomize-lock-wrapper

<FocusLock
  as="section"
  returnFocus
  className="settings-dialog"
  lockProps={{
    role: 'dialog',
    'aria-modal': true,
    'aria-labelledby': 'settings-title',
  }}
>
  <h2 id="settings-title">Settings</h2>
  <button>Save</button>
</FocusLock>

FocusLock renders a div by default; as changes the element and lockProps forwards attributes other than className.

Move focus without trapping itmove-focus-on-mount

import { MoveFocusInside } from 'react-focus-lock';

<MoveFocusInside>
  <input aria-label="Rename file" defaultValue="notes.txt" />
</MoveFocusInside>

MoveFocusInside forces focus into its child area on mount but does not keep later focus from leaving.

Move focus with arrow keysnavigate-focus-scope

import { useFocusScope } from 'react-focus-lock';

function ArrowNavigation() {
  const { focusNext, focusPrev } = useFocusScope();
  return (
    <div onKeyDown={(event) => {
      if (event.key === 'ArrowDown') { event.preventDefault(); void focusNext(); }
      if (event.key === 'ArrowUp') { event.preventDefault(); void focusPrev(); }
    }}>
      <button>One</button><button>Two</button><button>Three</button>
    </div>
  );
}

useFocusScope must run below a FocusLock, including a disabled one; its movement methods return promises.

Control a focus region without trappingcontrol-focus-without-lock

import { useRef } from 'react';
import { useFocusController } from 'react-focus-lock';

function Toolbar() {
  const ref = useRef(null);
  const focus = useFocusController(ref);
  return (
    <div ref={ref}>
      <button onClick={() => void focus.focusFirst()}>First item</button>
      <button>Second item</button>
    </div>
  );
}

useFocusController works without FocusLock and can combine several elements or refs into one programmatic scope.

Add guards around a scattered regionguard-portal-edge

import { InFocusGuard } from 'react-focus-lock';

<InFocusGuard />
<div ref={portalRef}>
  <button>Portaled action</button>
</div>
<InFocusGuard />

Guards become tabbable only when needed and can stop a shard at the end of the document from tabbing into browser chrome.

Alternatives

PackageRegistryPick it when
focus-trap-reactnpmChoose it for a React wrapper around focus-trap with its activation and fallback-focus option model
@radix-ui/react-focus-scopenpmChoose it when your UI already uses Radix primitives and you want the matching low-level focus scope
@headlessui/reactnpmChoose it when you need complete accessible dialogs, menus, and popovers rather than focus containment alone
react-focus-onnpmChoose it for the author's combined focus lock, scroll lock, and outside-content isolation behavior