focus-lock and focus-trap are both JavaScript libraries designed to confine keyboard focus within a specific DOM container — a critical accessibility requirement for modal dialogs, popovers, and other overlay components. They prevent users from tabbing out of the active UI region, ensuring screen reader and keyboard navigation remain predictable and usable.
Both focus-lock and focus-trap solve the same core problem: keeping keyboard focus trapped inside a modal or overlay so users can’t accidentally navigate into obscured background content. This is essential for WCAG compliance and a good experience for keyboard and screen reader users. But they take different approaches — one leans declarative and React-friendly, the other stays lightweight and framework-neutral.
focus-lock is built primarily for React and uses a declarative component model. You wrap your modal content in <FocusLock>, and the library handles setup and cleanup automatically when the component mounts or unmounts.
import { FocusLock } from 'focus-lock';
function MyModal() {
return (
<FocusLock returnFocus>
<div role="dialog" aria-modal="true">
<h2>Confirm Action</h2>
<button>Yes</button>
<button>No</button>
</div>
</FocusLock>
);
}
focus-trap provides an imperative API that works anywhere. You create a trap instance and explicitly call .activate() and .deactivate().
import { createFocusTrap } from 'focus-trap';
const modal = document.getElementById('my-modal');
const focusTrap = createFocusTrap(modal);
// Activate when modal opens
focusTrap.activate();
// Deactivate when modal closes
focusTrap.deactivate();
Both libraries support common options like returning focus to the triggering element and handling click outside events, but their configuration styles differ.
focus-lock passes options as props:
<FocusLock
returnFocus={true}
autoFocus={true}
allowOutsideClick={true}
noFocusGuards={false}
>
{/* modal content */}
</FocusLock>
focus-trap accepts an options object during creation:
const focusTrap = createFocusTrap(modal, {
returnFocusOnDeactivate: true,
clickOutsideDeactivates: true,
escapeDeactivates: true,
initialFocus: false
});
Note: focus-lock’s noFocusGuards adds invisible sentinel elements before and after the lock to catch focus — a technique also used internally by focus-trap, though not directly configurable as a boolean flag.
focus-lock ties activation directly to React rendering. When the <FocusLock> component appears in the tree, trapping begins; when it’s removed, trapping ends and focus optionally returns. This reduces boilerplate but assumes you’re using React’s render cycle to manage modal visibility.
focus-trap requires you to manually call .activate() and .deactivate(). This gives you precise control — useful if your modal’s open/closed state isn’t tied to component mount/unmount (e.g., toggling display: none instead of unmounting).
// Example: Toggling without unmounting
function toggleModal() {
if (modal.style.display === 'none') {
modal.style.display = 'block';
focusTrap.activate();
} else {
modal.style.display = 'none';
focusTrap.deactivate();
}
}
focus-lock is optimized for React. While it can be used outside React via its lower-level createFocusLock utility, its primary API assumes React context. It also ships with React-specific features like automatic cleanup on unmount.
focus-trap is completely framework-agnostic. It works in plain JavaScript, Vue, Angular, Svelte, or any environment with a DOM. There’s even a separate focus-trap-react package if you want a React wrapper around focus-trap.
Both support returning focus to the element that opened the modal.
In focus-lock, set returnFocus={true}:
<button onClick={() => setIsOpen(true)}>Open Modal</button>
{isOpen && (
<FocusLock returnFocus>
<Modal />
</FocusLock>
)}
In focus-trap, use returnFocusOnDeactivate: true:
const focusTrap = createFocusTrap(modal, {
returnFocusOnDeactivate: true
});
// The element that had focus when activate() was called gets focus back
focusTrap.activate();
Both libraries handle edge cases like modals with no focusable elements.
focus-lock will focus the container itself if no focusable children exist (unless autoFocus={false}).
focus-trap behaves similarly by default but allows overriding fallbackFocus to specify a fallback element:
createFocusTrap(modal, {
fallbackFocus: () => modal // or a specific element
});
For dynamically added content (e.g., loading states), both rely on the browser’s natural focus behavior once new focusable elements appear. Neither auto-re-focuses unless reactivated.
Both are lightweight and dependency-free. Neither pulls in large external libraries. However, focus-lock includes React-specific logic in its main entry point, so if you’re not using React, focus-trap will have a smaller effective footprint.
| Feature | focus-lock | focus-trap |
|---|---|---|
| Primary Use Case | React applications | Any JavaScript environment |
| API Style | Declarative (JSX component) | Imperative (.activate()/.deactivate()) |
| Lifecycle Handling | Automatic (tied to render) | Manual |
| Framework Coupling | Optimized for React | Framework-agnostic |
| Focus Guards | Configurable via noFocusGuards | Built-in, not directly configurable |
| Fallback Focus Control | Limited (container-focused by default) | Customizable via fallbackFocus |
Use focus-lock if you’re building a React app and want minimal boilerplate. Its component API aligns naturally with how modals are typically implemented in React (conditional rendering), and it handles cleanup reliably.
Use focus-trap if you’re not using React, need fine-grained control over when trapping starts/stops, or are integrating into a legacy codebase where modals aren’t managed by component lifecycles.
Both libraries are actively maintained, well-tested, and meet accessibility standards. Your choice should come down to your app’s architecture — not feature gaps. In fact, many teams use focus-trap inside a custom React hook to get the best of both worlds.
✅ Pro Tip: If you’re using React but still want
focus-trap’s flexibility, consider wrapping it in auseEffect:useEffect(() => { if (isOpen) { const trap = createFocusTrap(modalRef.current); trap.activate(); return () => trap.deactivate(); } }, [isOpen]);
Choose focus-lock if you're working in a React-heavy codebase and prefer a declarative, component-based API that integrates cleanly with React's lifecycle. It automatically handles mounting/unmounting and offers fine-grained control via props like returnFocus and noFocusGuards, making it ideal for complex UI trees where modals may nest or coexist.
Choose focus-trap if you need a framework-agnostic solution with explicit programmatic control over activation and deactivation. It’s well-suited for vanilla JavaScript apps, non-React frameworks, or scenarios requiring manual trap management (e.g., conditional trapping based on user interaction). Its imperative API gives you full control over when focus confinement starts and stops.
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