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.
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.
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>
);
}
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:
aria-hidden.// 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-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>;
}
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.| Scenario | Recommended Package |
|---|---|
| Building a React app with modals, drawers, or complex overlays | react-focus-lock |
| Working in a non-React environment (vanilla JS, Web Components, etc.) | focus-lock |
| Needing a dead-simple React modal with no extra features | react-focus-trap |
| Requiring WCAG 2.1+ compliance with robust edge-case handling | react-focus-lock |
| Avoiding React-specific abstractions for maximum control | focus-lock |
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.
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.
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.
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.
It is a trap! We got your focus and will not let him out!
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
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
lastActiveFocusis on the end user.
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:
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.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'
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);
Allows moving back and forth between focusable/tabbable elements
import { focusNextElement, focusPrevElement } from 'focus-lock';
focusNextElement(document.activeElement, {
scope: theBoundingDOMNode,
}); // -> next tabbable element
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
From MDN Article about accessible dialogs:
This one is about managing the focus.
I'v got a good article about focus management, dialogs and WAI-ARIA.
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.
default(topNode, lastNode) (aka setFocus), moves focus inside topNode, keeping in mind that last focus inside was - lastNode
MIT