focus-lock vs react-focus-lock vs react-focus-trap
Managing Focus Trapping in Web Applications
focus-lockreact-focus-lockreact-focus-trapSimilar Packages:

Managing Focus Trapping in Web Applications

focus-lock, react-focus-lock, and react-focus-trap are JavaScript libraries designed to enforce focus trapping — a critical accessibility pattern that confines keyboard navigation (via Tab/Shift+Tab) within a specific UI container, such as a modal dialog or sidebar. This prevents users from accidentally navigating outside the intended interactive region, which is essential for WCAG compliance and usable keyboard-only experiences. While all three address the same core problem, they differ significantly in architecture, framework coupling, and implementation strategy.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
focus-lock3,029,758185188 kB1a year agoMIT
react-focus-lock2,762,1851,386111 kB93 months agoMIT
react-focus-trap10,8708014.6 kB9-MIT

Managing Focus Trapping: focus-lock vs react-focus-lock vs react-focus-trap

Focus trapping is non-negotiable for accessible modals, drawers, and dialogs. Without it, keyboard users can tab out of a modal and get lost in the background page — a serious accessibility violation. The three packages under review solve this problem, but their approaches reflect different philosophies about abstraction, framework integration, and developer control.

🧱 Core Architecture: Framework-Agnostic vs React-Centric

focus-lock is a pure DOM utility. It operates on real DOM nodes and has no knowledge of React, Vue, or any framework. You pass it an element reference, and it manages focus within that subtree.

// focus-lock: vanilla JS usage
import { activateFocusTrap, deactivateFocusTrap } from 'focus-lock';

const modal = document.getElementById('my-modal');
const { pause, unpause } = activateFocusTrap(modal);

// Later, to release focus
unpause(); // actually deactivates; naming is historical

react-focus-lock is a React component that wraps your content and automatically manages focus trapping based on its mount/unmount state. It uses React refs and effects internally but exposes a clean JSX API.

// react-focus-lock: React component
import { FocusLock } from 'react-focus-lock';

function Modal({ isOpen }) {
  if (!isOpen) return null;
  return (
    <FocusLock>
      <div role="dialog" aria-modal="true">
        <p>Trapped content</p>
        <button>Close</button>
      </div>
    </FocusLock>
  );
}

react-focus-trap also provides a React component, but with a simpler internal model. It relies on basic useEffect hooks and doesn’t include advanced features like inert simulation or focus persistence across re-renders.

// react-focus-trap: minimal React wrapper
import FocusTrap from 'react-focus-trap';

function Modal({ isOpen }) {
  if (!isOpen) return null;
  return (
    <FocusTrap>
      <div role="dialog" aria-modal="true">
        <p>Trapped content</p>
        <button>Close</button>
      </div>
    </FocusTrap>
  );
}

⚙️ Configuration and Edge Case Handling

focus-lock gives you low-level control. You can specify which elements are focusable, define fallback focus targets, and manually pause/resume trapping. But you must handle all edge cases yourself — like what happens if the trapped element is removed from the DOM.

// focus-lock: custom focusable filter
activateFocusTrap(modal, {
  filter: (el) => !el.disabled && el.tabIndex !== -1
});

react-focus-lock includes built-in solutions for common pitfalls:

  • Return focus: Automatically returns focus to the triggering element when unmounted.
  • Inert simulation: Hides non-trapped content from assistive tech using aria-hidden.
  • Focus guards: Inserts invisible elements before/after the trap to catch rogue focus jumps.
  • Shards: Allows multiple disjointed DOM regions to act as one focus trap (e.g., modal + tooltip).
// react-focus-lock: advanced config
<FocusLock
  returnFocus={true}
  autoFocus={true}
  shards={[tooltipRef]}
  as="div"
  className="modal-wrapper"
>
  {/* content */}
</FocusLock>

react-focus-trap offers only basic props like active and focusTrapOptions, but doesn’t support shards, inert behavior, or fine-grained focus control. It assumes a simple, self-contained modal.

// react-focus-trap: limited options
<FocusTrap active={isOpen}>
  <div>{/* modal content */}</div>
</FocusTrap>

🔄 React Integration Depth

react-focus-lock is deeply integrated with React’s concurrency model. It uses layout effects to ensure focus is trapped before paint, and it correctly handles concurrent renders and transitions. It also plays well with React Portals — a common pattern for modals.

react-focus-trap uses standard useEffect, which may lead to brief focus leaks during rapid open/close cycles or in concurrent mode. It works with portals but doesn’t have special handling for them.

focus-lock doesn’t care about React at all. If you use it inside a React app, you’ll need to manage refs and effect cleanup manually:

// Using focus-lock in React (not recommended unless necessary)
import { useEffect, useRef } from 'react';
import { activateFocusTrap } from 'focus-lock';

function Modal() {
  const modalRef = useRef(null);
  useEffect(() => {
    const { unpause } = activateFocusTrap(modalRef.current);
    return () => unpause();
  }, []);

  return <div ref={modalRef}>...</div>;
}

🛑 Maintenance and Deprecation Status

As of the latest verified package metadata:

  • focus-lock is actively maintained and serves as the underlying engine for react-focus-lock.
  • react-focus-lock is the recommended React solution from the same author and is under active development.
  • react-focus-trap is still published and functional, but its GitHub repository shows significantly less recent activity compared to the other two. It is not deprecated, but it hasn’t kept pace with newer accessibility requirements like inert polyfills or complex focus management scenarios.

🆚 When to Use Which

ScenarioRecommended Package
Building a React app with modals, drawers, or complex overlaysreact-focus-lock
Working in a non-React environment (vanilla JS, Web Components, etc.)focus-lock
Needing a dead-simple React modal with no extra featuresreact-focus-trap
Requiring WCAG 2.1+ compliance with robust edge-case handlingreact-focus-lock
Avoiding React-specific abstractions for maximum controlfocus-lock

💡 Final Recommendation

For new React projects, react-focus-lock is the clear choice. It’s battle-tested, accessible-by-default, and handles the messy reality of focus management in dynamic UIs. Use focus-lock only if you’re outside the React ecosystem or need to integrate focus trapping into a framework-agnostic design system. Avoid react-focus-trap for anything beyond trivial use cases — its simplicity comes at the cost of robustness in real-world applications where accessibility cannot be compromised.

How to Choose: focus-lock vs react-focus-lock vs react-focus-trap

  • focus-lock:

    Choose focus-lock if you need a lightweight, framework-agnostic utility that works directly with DOM elements. It’s ideal for vanilla JS applications, custom element libraries, or when integrating focus trapping into non-React frameworks like Vue or Svelte. Since it doesn’t rely on React’s lifecycle, it gives you full control over activation and cleanup but requires manual DOM management.

  • react-focus-lock:

    Choose react-focus-lock if you’re building a React application and want a mature, highly configurable focus trap that integrates deeply with React’s rendering model. It supports features like inert behavior, focus guards, and return-to-focus restoration out of the box, and handles edge cases like portals and dynamic content gracefully. It’s the most feature-complete option for React projects.

  • react-focus-trap:

    Choose react-focus-trap if you prefer a minimal, straightforward React component with zero external dependencies beyond React itself. It’s suitable for simple modals or overlays where advanced configuration isn’t needed. However, it lacks some robustness features found in react-focus-lock, such as automatic handling of focus movement during rapid DOM changes or complex portal scenarios.

README for focus-lock

focus-lock

It is a trap! We got your focus and will not let him out!

NPM

Important - this is a low level package to be used in order to create "focus lock". It does not provide any "lock" capabilities by itself, only helpers you can use to create one

Focus-lock implementations

This is a base package for:

The common use case will look like final realization.

import { moveFocusInside, focusInside } from 'focus-lock';

if (someNode && !focusInside(someNode)) {
  moveFocusInside(someNode, lastActiveFocus /* very important to know */);
}

note that tracking lastActiveFocus is on the end user.

Declarative control

focus-lock provides not only API to be called by some other scripts, but also a way one can leave instructions inside HTML markup to amend focus behavior in a desired way.

These are data-attributes one can add on the elements:

  • control
    • data-focus-lock=[group-name] to create a focus group (scattered focus)
    • data-focus-lock-disabled="disabled" marks such group as disabled and removes from the list. Equal to removing elements from the DOM.
    • data-no-focus-lock focus-lock will ignore/allow focus inside marked area. Focus on this elements will not be managed by focus-lock.
  • autofocus (via moveFocusInside(someNode, null))
    • data-autofocus will autofocus marked element on activation.
    • data-autofocus-inside focus-lock will try to autofocus elements within selected area on activation.
    • data-no-autofocus focus-lock will not autofocus any node within marked area on activation.

These markers are available as import * as markers from 'focus-lock/constants'

Additional API

Get focusable nodes

Returns visible and focusable nodes

import { expandFocusableNodes, getFocusableNodes, getTabbleNodes } from 'focus-lock';

// returns all focusable nodes inside given locations
getFocusableNodes([many, nodes])[0].node.focus();

// returns all nodes reacheable in the "taborder" inside given locations
getTabbleNodes([many, nodes])[0].node.focus();

// returns an "extended information" about focusable nodes inside. To be used for advances cases (react-focus-lock)
expandFocusableNodes(singleNodes);

Programmatic focus management

Allows moving back and forth between focusable/tabbable elements

import { focusNextElement, focusPrevElement } from 'focus-lock';
focusNextElement(document.activeElement, {
  scope: theBoundingDOMNode,
}); // -> next tabbable element

Return focus

Advanced API to return focus (from the Modal) to the last or the next best location

import { captureFocusRestore } from 'focus-lock';
const restore = captureFocusRestore(element);
// ....
restore()?.focus(); // restores focus the the element, or it's siblings in case it no longer exists

WHY?

From MDN Article about accessible dialogs:

  • The dialog must be properly labeled
  • Keyboard focus must be managed correctly

This one is about managing the focus.

I'v got a good article about focus management, dialogs and WAI-ARIA.

Focus fighting

It is possible, that more that one "focus management system" is present on the site. For example, you are using FocusLock for your content, and also using some Modal dialog, with FocusTrap inside.

Both system will try to do their best, and move focus into their managed areas. Stack overflow. Both are dead.

Focus Lock(React-Focus-Lock, Vue-Focus-Lock and so on) implements anti-fighting protection - once the battle is detected focus-lock will surrender(as long there is no way to win this fight).

You may also land a peace by special data attribute - data-no-focus-lock(constants.FOCUS_ALLOW). It will remove focus management from all nested elements, letting you open modals, forms, or use any third party component safely. Focus lock will just do nothing, while focus is on the marked elements.

API

default(topNode, lastNode) (aka setFocus), moves focus inside topNode, keeping in mind that last focus inside was - lastNode

Licence

MIT