focus-lock、react-focus-lock、react-focus-trap はすべて、モーダルやダイアログなどの UI コンポーネント内でキーボードフォーカスを制限(トラップ)し、アクセシビリティを確保するためのライブラリです。これらは特にスクリーンリーダー利用者やキーボードナビゲーションに依存するユーザーにとって重要な機能を提供します。focus-lock はフレームワーク非依存の低レベル実装であり、react-focus-lock はその React 向けラッパー、react-focus-trap は独立した React 専用実装です。
モーダルやサイドバーなど、画面の一部を一時的にロックして操作を制限する UI パターンでは、キーボードフォーカスがその領域内に閉じ込められる(フォーカストラップ)必要があります。これは WCAG 2.1 の Success Criterion 2.4.3 (Focus Order) や 2.1.2 (No Keyboard Trap) に準拠するために不可欠です。focus-lock、react-focus-lock、react-focus-trap はこの課題を解決する代表的な npm パッケージですが、設計思想や使用方法に明確な違いがあります。
focus-lock は純粋な JavaScript ライブラリで、DOM 操作とフォーカス制御のロジックを提供します。React に依存せず、あらゆるフロントエンド環境で使用可能です。
// focus-lock: 手動で DOM 要素を指定
import { activateFocusTrap, deactivateFocusTrap } from 'focus-lock';
const trap = activateFocusTrap(document.getElementById('modal'));
// ... モーダルを閉じる際に
deactivateFocusTrap(trap);
react-focus-lock は focus-lock を内部で使用し、React のコンポーネントモデルに適合させたラッパーです。useEffect や createPortal との連携が組み込まれています。
// react-focus-lock: コンポーネントとして使用
import { FocusLock } from 'react-focus-lock';
function Modal() {
return (
<FocusLock>
<div id="modal">
<button>閉じる</button>
</div>
</FocusLock>
);
}
react-focus-trap はゼロから React 向けに設計された独立実装で、外部依存なしにフォーカストラップ機能を提供します。
// react-focus-trap: コンポーネントまたはフック
import FocusTrap from 'react-focus-trap';
function Modal() {
return (
<FocusTrap>
<div id="modal">
<button>閉じる</button>
</div>
</FocusTrap>
);
}
focus-lock は低レベル API のため、すべての動作を手動で制御する必要があります。例えば、フォーカス可能な要素の検出ロジックや、Tab キー押下時の挙動を細かく調整できますが、そのためのコードは開発者が記述する必要があります。
react-focus-lock は豊富なプロパティを提供します。returnFocus(閉じた後に元のフォーカス位置に戻す)、shards(複数の DOM 領域を1つのトラップとして扱う)、group(複数のトラップ間の優先順位制御)などが特徴です。
// react-focus-lock: 高度な設定例
<FocusLock
returnFocus
shards={[sidebarRef, modalRef]}
group="modal-group"
>
{/* コンテンツ */}
</FocusLock>
react-focus-trap はシンプルなオプションに焦点を当てています。active(有効/無効の切り替え)、focusTrapOptions(内部の focus-trap ライブラリへのオプション渡し)が主な設定項目です。
// react-focus-trap: 基本的な設定
<FocusTrap active={isOpen}>
<div>{/* コンテンツ */}</div>
</FocusTrap>
focus-lock は React のライフサイクルとは無関係のため、useEffect や useLayoutEffect で手動でアクティベート/デアクティベートする必要があります。アンマウント時のクリーンアップ漏れが発生しやすい点に注意が必要です。
// focus-lock + React: 手動統合
import { useEffect, useRef } from 'react';
import { activateFocusTrap, deactivateFocusTrap } from 'focus-lock';
function Modal({ isOpen }) {
const modalRef = useRef();
const trapRef = useRef();
useEffect(() => {
if (isOpen) {
trapRef.current = activateFocusTrap(modalRef.current);
}
return () => {
if (trapRef.current) {
deactivateFocusTrap(trapRef.current);
}
};
}, [isOpen]);
return <div ref={modalRef}>{/* ... */}</div>;
}
react-focus-lock と react-focus-trap はどちらも React のマウント/アンマウント時に自動でフォーカストラップを管理します。ただし、react-focus-lock はポータル(ReactDOM.createPortal)内のコンテンツにも対応しており、より複雑な UI 構成をサポートします。
モーダルは通常、ReactDOM.createPortal を使って body 直下にレンダリングされます。この場合、フォーカストラップライブラリはポータル外の DOM 要素を正しく処理できる必要があります。
react-focus-lock はポータルをネイティブにサポートしています。FocusLock コンポーネントをポータル内に配置するだけで、正しく動作します。
// react-focus-lock + Portal
function Modal({ isOpen }) {
if (!isOpen) return null;
return createPortal(
<FocusLock>
<div>モーダルコンテンツ</div>
</FocusLock>,
document.body
);
}
react-focus-trap もポータルに対応していますが、内部で MutationObserver を使用して DOM 変更を監視するため、極めて動的なコンテンツではパフォーマンスに影響が出る可能性があります。
focus-lock はポータルに対応していません。開発者が手動でポータルの DOM 要素を取得し、アクティベートする必要があります。
基本的なモーダルで、開閉時にフォーカスを内部に閉じ込める必要がある場合。
react-focus-trap が最も簡潔です。import FocusTrap from 'react-focus-trap';
function SimpleModal({ isOpen, onClose }) {
if (!isOpen) return null;
return (
<FocusTrap>
<div role="dialog" aria-modal="true">
<p>コンテンツ</p>
<button onClick={onClose}>閉じる</button>
</div>
</FocusTrap>
);
}
メインモーダルの上に確認ダイアログが表示され、さらに左側に固定サイドバーがある複雑な UI。
react-focus-lock の shards と group 機能が活きます。// サイドバーとモーダルを1つのトラップとして扱う
<FocusLock shards={[sidebarRef, modalRef]}>
{/* ... */}
</FocusLock>
// 別のグループのトラップ(例:確認ダイアログ)
<FocusLock group="alert">
{/* ... */}
</FocusLock>
Vue.js や Svelte で同じフォーカス制御ロジックを使いたい場合。
focus-lock が唯一の選択肢です。// Vue の mounted / beforeUnmount で使用
mounted() {
this.trap = activateFocusTrap(this.$refs.modal);
},
beforeUnmount() {
deactivateFocusTrap(this.trap);
}
| ライブラリ | 最適なシナリオ | 注意点 |
|---|---|---|
focus-lock | React 以外の環境、または低レベル制御が必要な場合 | React では手動統合が必要で、ミスしやすい |
react-focus-lock | 複雑な UI(ネスト、ポータル、複数領域)を扱う大規模アプリ | 学習コストがやや高い |
react-focus-trap | 単純なモーダルやダイアログが中心のプロジェクト | 高度なカスタマイズには向かない |
最終的には、プロジェクトの規模、UI の複雑さ、そして将来の拡張可能性を考慮して選択してください。アクセシビリティは「後から追加」ではなく「最初から設計」するものであることを忘れないでください。
focus-lock は React 以外の環境(例:Vanilla JS、Vue、Svelte)でフォーカストラップ機能が必要な場合に最適です。内部ロジックを直接制御したい高度なユースケースや、軽量なコア機能だけを必要とする場合に選択してください。ただし、React での使用には追加の統合コードが必要になるため、React 専用プロジェクトでは通常推奨されません。
react-focus-lock は focus-lock の公式 React ラッパーであり、React のライフサイクルと完全に統合されています。コンポーネントベースの API、カスタムフック、および高度な設定オプション(例:shards、grouping)を提供するため、複雑な UI(例:ネストされたモーダル、ポータル付きコンポーネント)を扱う大規模 React アプリケーションに最適です。既存の focus-lock ユーザーが React に移行する場合にも自然な選択肢です。
react-focus-trap はシンプルで直感的な API を重視する開発者向けです。React 16.8+ のフックを活用しており、基本的なフォーカストラップ要件(例:単一モーダル、シンプルなダイアログ)に迅速に対応できます。高度なカスタマイズや特殊なユースケースが不要な場合、学習コストが低く、導入が容易なため、小〜中規模プロジェクトに適しています。
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