{"id":15030425,"url":"https://github.com/thekashey/react-focus-lock","last_synced_at":"2025-05-10T05:20:50.746Z","repository":{"id":37601637,"uuid":"101814136","full_name":"theKashey/react-focus-lock","owner":"theKashey","description":"It is a trap! A lock for a Focus. 🔓","archived":false,"fork":false,"pushed_at":"2025-05-08T08:06:50.000Z","size":3682,"stargazers_count":1348,"open_issues_count":5,"forks_count":72,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-05-08T20:58:06.175Z","etag":null,"topics":["a11y","focus","focus-lock","modal-dialogs","react"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/theKashey.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2017-08-29T22:56:18.000Z","updated_at":"2025-05-08T09:36:54.000Z","dependencies_parsed_at":"2024-01-22T18:56:02.815Z","dependency_job_id":"5a4f16e8-efc2-4af5-9941-2658da267dd7","html_url":"https://github.com/theKashey/react-focus-lock","commit_stats":{"total_commits":369,"total_committers":33,"mean_commits":"11.181818181818182","dds":0.2710027100271003,"last_synced_commit":"1ce25bcff6bc28da755d930202ef341d4d97b243"},"previous_names":[],"tags_count":101,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/theKashey%2Freact-focus-lock","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/theKashey%2Freact-focus-lock/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/theKashey%2Freact-focus-lock/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/theKashey%2Freact-focus-lock/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/theKashey","download_url":"https://codeload.github.com/theKashey/react-focus-lock/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253157511,"owners_count":21863129,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["a11y","focus","focus-lock","modal-dialogs","react"],"created_at":"2024-09-24T20:13:18.129Z","updated_at":"2025-05-08T22:30:32.060Z","avatar_url":"https://github.com/theKashey.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"left\"\u003e\n  \u003ch1 align=\"center\"\u003eREACT FOCUS LOCK\u003c/h1\u003e\n  \u003cimg src=\"./assets/ackbar.png\" alt=\"it-is-a-trap\" width=\"200\" height=\"200\" align=\"right\"\u003e\n  \n  - browser friendly focus lock\u003cbr/\u003e\n  - matching all your use cases\u003cbr/\u003e\n  - trusted by best UI frameworks\u003cbr/\u003e\n  - the thing Admiral Ackbar was talking about\u003cbr/\u003e\n  \u003cbr/\u003e\n\n[![CircleCI status](https://img.shields.io/circleci/project/github/theKashey/react-focus-lock/master.svg?style=flat-square)](https://circleci.com/gh/theKashey/react-focus-lock/tree/master)\n[![npm](https://img.shields.io/npm/v/react-focus-lock.svg)](https://www.npmjs.com/package/react-focus-lock)\n[![bundle size](https://badgen.net/bundlephobia/minzip/react-focus-lock)](https://bundlephobia.com/result?p=react-focus-lock)\n[![downloads](https://badgen.net/npm/dm/react-focus-lock)](https://www.npmtrends.com/react-focus-lock)\n  \u003chr/\u003e  \n\u003c/div\u003e\n\nIt is a trap! We got your focus and will not let him out!\n\n- Modal dialogs. You can not leave it with \"Tab\", ie do a \"tab-out\".\n- Focused tasks. It will aways brings you back, as you can \"lock\" user inside a component.\n- Any any other case, when you have to lock user _intention_ and _focus_, if that's what `a11y` is asking for.\n   - Including programatic focus management and smart return focus \n\n### Trusted\nTrusted by \n[Atlassian AtlasKit](https://atlaskit.atlassian.com), \n[ReachUI](https://ui.reach.tech/), \n[SmoothUI](https://smooth-ui.smooth-code.com/), \n[Storybook](https://storybook.js.org/)\nand we will do our best to earn your trust too!\n \n# Features\n - no keyboard control, everything is done watching a __focus behavior__, not emulating it. Focus-Locks works for all cases including positive tab indexes.\n - React __Portals__ support. Even if some data is in outer space - it is [still in lock](https://github.com/theKashey/react-focus-lock/issues/19).\n - _Scattered_ locks, or focus lock groups - you can setup different isolated locks, and _tab_ from one to another.\n - Controllable isolation level.\n - variable size bundle. Uses _sidecar_ to trim UI part down to 1.5kb. \n \n\u003e 💡 __focus__ locks is part of a bigger whole, consider __scroll lock__ and __text-to-speech__ lock\nyou have to use to really \"lock\" the user.\nTry [react-focus-on](https://github.com/theKashey/react-focus-on) to archive everything above, assembled in the right order. \n \n# How to use\nJust wrap something with focus lock, and focus will be `moved inside` on mount.\n```js\n import FocusLock from 'react-focus-lock';\n\n const JailForAFocus = ({onClose}) =\u003e (\n    \u003cFocusLock\u003e\n      You can not leave this form\n      \u003cbutton onClick={onClose} /\u003e\n    \u003c/FocusLock\u003e\n );\n```\nDemo - https://codesandbox.io/s/5wmrwlvxv4.\n\n# API\n\u003e FocusLock would work perfectly even with no props set.\n\n FocusLock has few props to tune behavior, all props are optional:\n  - `disabled`, to disable(enable) behavior without altering the tree.\n  - `className`, to set the `className` of the internal wrapper.\n  - `returnFocus`, to return focus into initial position on unmount\n\u003e By default `returnFocus` is disabled, so FocusLock __will not__ restore original focus on deactivation.\n\u003e This was done mostly to avoid breaking changes. We __strong recommend enabling it__, to provide a better user experience.\n    \n  This is expected behavior for Modals, but it is better to implement it by your self. See [unmounting and focus management](https://github.com/theKashey/react-focus-lock#unmounting-and-focus-management) for details\n  - `persistentFocus=false`, requires any element to be focused. This also disables text selections inside, and __outside__ focus lock.\n  - `autoFocus=true`, enables or disables focusing into on Lock activation. If disabled Lock will blur an active focus.\n  - `noFocusGuards=false` disabled _focus guards_ - virtual inputs which secure tab index.\n  - `group='''` named focus group for focus scattering aka [combined lock targets](https://github.com/theKashey/vue-focus-lock/issues/2)\n  - `shards=[]` an array of `ref` pointing to the nodes, which focus lock should consider and a part of it. This is another way focus scattering.  \n  - `whiteList=fn` you could _whitelist_ locations FocusLock should carry about. Everything outside it will ignore. For example - any modals.\n  - `as='div'` if you need to change internal `div` element, to any other. Use ref forwarding to give FocusLock the node to work with.\n  - `lockProps={}` to pass any extra props (except className) to the internal wrapper.\n  - `hasPositiveIndices=false` to support a focus lock behavior when any elements tabIndex greater than 0.\n  - `crossFrame=true` enables aggressive focus capturing within iframes\n\n## Programmatic control\nFocus lock exposes a few methods to control focus programmatically.\n### Imperative API\n- `useFocusInside(nodeRef)` - to move focus inside the given node\n- `useFocusScope():{autofocus, focusNext, focusPrev}` - provides API to manage focus within the current lock\n- `useFocusState()` - manages focus state of a given node\n- `useFocusController(nodeRef)` - low level version of `useFocusScope` working without FocusLock \n\n### Declarative API\n- `\u003cAutoFocusInside/\u003e` - causes autofocus to look inside the component\n- `\u003cMoveFocusInside/\u003e` - wrapper around `useFocusInside`, forcibly moves focus inside on mount\n- `\u003cFreeFocusInside/\u003e` - hides internals from FocusLock allowing unmanaged focus\n\n#### Indirect API\nFocus-lock behavior can be controlled via data-attributes. Declarative API above is working by setting them for you.\nSee [corresponding section in focus-lock](https://github.com/theKashey/focus-lock#declarative-control) for details\n\n\n\n### Focusing in OSX (Safari/Firefox) is strange!\nBy default `tabbing` in OSX `sees` only controls, but not links or anything else `tabbable`. This is system settings, and Safari/Firefox obey.\nPress Option+Tab in Safari to loop across all tabbables, or change the Safari settings. There is no way to _fix_ Firefox, unless change system settings (Control+F7). See [this issue](https://github.com/theKashey/react-focus-lock/issues/24) for more information.\n\n## Set up\n### Requirements\n- version 1x is React 15/16 compatible\n- version 2+ requires React 16.8+ (hooks)\n### Import\n`react-focus-lock` exposed __3 entry points__: for the classical usage, and a _sidecar_ one.\n#### Default usage\n- 4kb, `import FocusLock from 'react-focus-lock` would give you component you are looking for.\n\n#### Separated usage\nMeanwhile - you dont need any focus related logic until it's needed.\nThus - you may defer that logic till Lock activation and move all related code to a _sidecar_.\n\n- UI, __1.5kb__, `import FocusLockUI from 'react-focus-lock/UI` - a DOM part of a lock.\n- Sidecar, 3.5kb, `import Sidecar from 'react-focus-lock/sidecar` - which is the real focus lock.\n\n```js\nimport FocusLockUI from \"react-focus-lock/UI\";\nimport {sidecar} from \"use-sidecar\";\n\n// prefetch sidecar. data would be loaded, but js would not be executed\nconst FocusLockSidecar = sidecar(  \n  () =\u003e import(/* webpackPrefetch: true */ \"react-focus-lock/sidecar\")\n);\n\n\u003cFocusLockUI\n    disabled={this.state.disabled}\n    sideCar={FocusLockSidecar}\n\u003e\n {content}\n\u003c/FocusLockUI\u003e \n```\nThat would split FocusLock into two pieces, reducing app size and improving the first load.\nThe cost of focus-lock is just 1.5kb!\n\n\u003e Saved 3.5kb?! 🤷‍♂️ 3.5kb here and 3.5kb here, and your 20mb bundle is ready.\n\n# Autofocus\n Use when you cannot use the native `autoFocus` prop - because you only want to autofocus once the Trap has been activated\n      \n - prop `data-autofocus` on the element.\n - prop `data-autofocus-inside` on the element to focus on something inside.\n - `AutoFocusInside` component, as named export of this library.\n```js\n import FocusLock, { AutoFocusInside } from 'react-focus-lock';\n \n \u003cFocusLock\u003e\n   \u003cbutton\u003eClick\u003c/button\u003e\n   \u003cAutoFocusInside\u003e\n    \u003cbutton\u003ewill be focused\u003c/button\u003e\n   \u003c/AutoFocusInside\u003e\n \u003c/FocusLock\u003e\n // is the same as\n \n \u003cFocusLock\u003e\n   \u003cbutton\u003eClick\u003c/button\u003e\n    \u003cbutton data-autofocus\u003ewill be focused\u003c/button\u003e\n \u003c/FocusLock\u003e\n ```\n \n If there is more than one auto-focusable target - the first will be selected.\n If it is a part of radio group, and __rest of radio group element are also autofocusable__(just put them into AutoFocusInside) - \n checked one fill be selected.\n \n `AutoFocusInside` will work only on Lock activation, and does nothing, then used outside of the lock.\n You can use `MoveFocusInside` to move focus inside with or without lock.\n \n```js\n import { MoveFocusInside } from 'react-focus-lock';\n    \n \u003cMoveFocusInside\u003e\n  \u003cbutton\u003ewill be focused\u003c/button\u003e\n \u003c/MoveFocusInside\u003e\n ```\n \n# Portals\nUse focus scattering to handle portals\n\n- using `groups`. Just create a few locks (only one could be active) with a same group name\n```js\nconst PortaledElement = () =\u003e (\n   \u003cFocusLock group=\"group42\" disabled={true}\u003e\n     // \"discoverable\" portaled content\n   \u003c/FocusLock\u003e  \n);\n\n\u003cFocusLock group=\"group42\"\u003e\n  // main content\n\u003c/FocusLock\u003e\n```\n- using `shards`. Just pass all the pieces to the \"shards\" prop. \n```js\nconst PortaledElement = () =\u003e (\n   \u003cdiv ref={ref}\u003e\n     // \"discoverable\" portaled content\n   \u003c/div\u003e  \n);\n\n\u003cFocusLock shards={[ref]}\u003e\n  // main content\n\u003c/FocusLock\u003e\n```\n- without anything. FocusLock will not prevent focusing portaled element, but will not include them in to tab order \n```js\nconst PortaledElement = () =\u003e (\n   \u003cdiv\u003e\n     // NON-\"discoverable\" portaled content\n   \u003c/div\u003e  \n);\n\n\u003cFocusLock shards={[ref]}\u003e\n  // main content\n  \u003cPortaledElement /\u003e\n\u003c/FocusLock\u003e\n```\n\n### Using your own `Components`\nYou may use `as` prop to change _what_ Focus-Lock will render around `children`.\n```js\n\u003cFocusLock as=\"section\"\u003e\n    \u003cbutton\u003eClick\u003c/button\u003e\n    \u003cbutton data-autofocus\u003ewill be focused\u003c/button\u003e\n \u003c/FocusLock\u003e\n \n \u003cFocusLock as={AnotherComponent} lockProps={{anyAnotherComponentProp: 4}}\u003e\n    \u003cbutton\u003eClick\u003c/button\u003e\n    \u003cspan\u003eHello there!\u003c/span\u003e\n \u003c/FocusLock\u003e\n``` \n\n### Programmatic Control\nLet's take a look at the `Rowing Focus` as an example. \n```tsx\n// Will set tabindex to -1 when is not focused\nconst FocusTrackingButton = ({ children }) =\u003e {\n    const { active, onFocus, ref } = useFocusState();\n    return (\n        \u003cbutton tabIndex={active ? undefined : -1} onFocus={onFocus} ref={ref}\u003e\n            {children}\n        \u003c/button\u003e\n    );\n};\nconst RowingFocusInternalTrap = () =\u003e {\n    const { autoFocus, focusNext, focusPrev } = useFocusScope();\n    // use useFocusController(divRef) if there is no FocusLock around\n\n    useEffect(() =\u003e {\n        autoFocus();\n    }, []);\n\n    const onKey = (event) =\u003e {\n        if (event.key === 'ArrowDown') {\n            focusNext({ onlyTabbable: false });\n        }\n        if (event.key === 'ArrowUp') {\n            focusPrev({ onlyTabbable: false });\n        }\n    };\n    \n    return (\n        \u003cdiv\n            onKeyDown={onKey}\n            // ref={divRef} for  useFocusController\n        \u003e\n            \u003cFocusButton\u003eButton1\u003c/FocusButton\u003e\n            \u003cFocusButton\u003eButton2\u003c/FocusButton\u003e\n            \u003cFocusButton\u003eButton3\u003c/FocusButton\u003e\n            \u003cFocusButton\u003eButton4\u003c/FocusButton\u003e\n        \u003c/div\u003e\n    );\n};\n\n// FocusLock, even disabled one\nconst RowingFocusTrap = () =\u003e (\n    \u003cFocusLock disabled\u003e\n        \u003cRowingFocusInternalTrap /\u003e\n    \u003c/FocusLock\u003e\n);\n```\n\n### Guarding\nAs you may know - FocusLock is adding `Focus Guards` before and after lock to remove some side effects, like page scrolling.\nBut `shards` will not have such guards, and it might be not so cool to use them - for example if no `tabbable` would be\ndefined after shard - you will tab to the browser chrome.\n\nYou may wrap shard  with `InFocusGuard` or just drop `InFocusGuard` here and there - that would solve the problem.\n```js\nimport {InFocusGuard} from 'react-focus-lock';\n\n// wrap with\n\u003cInFocusGuard\u003e\n  \u003cbutton /\u003e\n\u003c/InFocusGuard\u003e\n\n// place before and after\n\u003cInFocusGuard /\u003e\n\u003cbutton /\u003e\n\u003cInFocusGuard /\u003e\n```\nInFocusGuards would be active(tabbable) only when tabble, it protecting, is focused.\n\n#### Removing Tailing Guard\nIf only your modal is the last tabble element on the body - you might remove the Tailing Guard,\nto allow user _tab_ into address bar.\n```js\n\u003cInFocusGuard/\u003e\n\u003cbutton /\u003e  \n// there is no \"tailing\" guard :)\n```\n \n# Unmounting and focus management\n - In case FocusLock has `returnFocus` enabled, and it's going to be unmounted - focus will be returned after zero-timeout.\n - In case `returnFocus` is set to `false`, and you are going to control focus change on your own - keep in mind\n \u003e\u003e React will first call Parent.componentWillUnmount, and next Child.componentWillUnmount\n \n This means - Trap will be still active by the time you _may_ want move(return) focus on componentWillUnmount. Please deffer this action with a zero-timeout.\n \n Similarly, if you are using the `disabled` prop to control FocusLock, you will need a zero-timeout to correctly restore focus.\n \n```\n\u003cFocusLock\n  disabled={isFocusLockDisabled}\n  onDeactivation={() =\u003e {\n    // Without the zero-timeout, focus will likely remain on the button/control\n    // you used to set isFocusLockDisabled = true\n    window.setTimeout(() =\u003e myRef.current.focus(), 0);\n  }\n\u003e\n```\n\n## Return focus to another node\nIn some cases the original node that was focused before the lock was activated is not the desired node to return focus to.\nSome times this node might not exists at all.\n\n- first of all, FocusLock need a moment to record this node, please do not hide it onClick, but hide onBlur (Dropdown, looking at you)\n- second, you may specify a callback as `returnFocus`, letting you decide where to return focus to.\n```tsx\n\u003cFocusLock\n    returnFocus={(suggestedNode) =\u003e {\n        // somehow activeElement should not be changed\n        if(document.activeElement.hasAttributes('main-content')) {\n            // opt out from default behavior\n            return false;\n        }\n        if (someCondition(suggestedNode)) {\n            // proceed with the suggested node\n            return true;\n        } \n        // handle return focus manually\n        document.getElementById('the-button').focus();\n        // opt out from default behavior\n        return false;\n    }}\n/\u003e\n````\n\n## Return focus with no scroll\n\u003e read more at the [issue #83](https://github.com/theKashey/react-focus-lock/issues/83) or\n[mdn article](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus).\n\nTo return focus, but without _jumpy_ page scroll returning a focus you might specify a focus option\n```js\n\u003cFocusLock\n  returnFocus={{ preventScroll: false }} // working not in all browsers\n\u003e   \n```  \nNot supported by Edge and Safari.\n\n## Focus fighting\nTwo different _focus-lock-managers_ or even different version of a single one, being active\nsimultaneously will FIGHT for the focus. This usually totally breaks user experience.\n\n__React-Focus-Lock will automatically surrender__, letting another library to take the lead.\n\n### Resolving focus fighting\nYou may wrap some render branch with `FreeFocusInside`, and react-focus-lock __will ignore__\nany focus inside marked node. So in case focus moves to _uncontrolled location_ focus-lock will not trigger letting another library to act without interference in that another location.\n\n```js\nimport { FreeFocusInside } from 'react-focus-lock';\n\n\u003cFreeFocusInside\u003e\n \u003cdiv id=\"portal-for-modals\"\u003e\n   in this div i am going to portal my modals, dont fight with them please\n \u003c/div\u003e\n\u003c/FreeFocusInside\u003e\n```\nAnother option for hybrid applications is to `whiteList` area where Focus-Lock should act, automatically allowing other managers in other areas.\nThe code below will scope Focus-Lock on inside the (react)`root` element, so anything jQuery can add to the body will be ignored.\n```js\n\u003cFocusLock whiteList={node =\u003e document.getElementById('root').contains(node)}\u003e\n ...\n\u003c/FocusLock\u003e\n```\n\n### Two Focus-Locks\nReact-Focus-Lock is expected to be a singleton.\n__Use webpack or yarn resolution for force only one version of react-focus-lock used.\n\n\u003e webpack.conf\n```js\n resolve: {    \n    alias: {\n      'react-focus-lock': path.resolve(path.join(__dirname, './node_modules/react-focus-lock'))\n ...\n```\n\n# WHY?\nFrom [MDN Article about accessible dialogs](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_dialog_role):\n- The dialog must be properly labeled\n- Keyboard __focus must be managed__ correctly\n\nThis one is about managing the focus.\n\nI've got a good [article about focus management, dialogs and  WAI-ARIA](https://medium.com/@antonkorzunov/its-a-focus-trap-699a04d66fb5).\n\n# Not only for React\nUses [focus-lock](https://github.com/theKashey/focus-lock/) under the hood. It does also provide support for Vue.js and Vanilla DOM solutions\n\n# More\nTo create a \"right\" modal dialog you have to:\n- manage a focus. Use this library\n- block document scroll. Use [react-remove-scroll](https://github.com/theKashey/react-remove-scroll).\n- hide everything else from screen readers. Use [aria-hidden](https://github.com/theKashey/aria-hidden)\n\nYou may use [react-focus-on](https://github.com/theKashey/react-focus-on) to achieve everything above, assembled in the right order.\n\n# Licence\n MIT\n \n \n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthekashey%2Freact-focus-lock","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthekashey%2Freact-focus-lock","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthekashey%2Freact-focus-lock/lists"}