{"id":13727155,"url":"https://github.com/discord/focus-layers","last_synced_at":"2025-04-04T07:05:13.041Z","repository":{"id":42864129,"uuid":"259172459","full_name":"discord/focus-layers","owner":"discord","description":"Tiny React hooks for isolating focus within subsections of the DOM.","archived":false,"fork":false,"pushed_at":"2023-07-18T21:55:27.000Z","size":1057,"stargazers_count":298,"open_issues_count":16,"forks_count":16,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-03-28T06:03:34.911Z","etag":null,"topics":["a11y","dialog","focus-lock","focus-trap","modals","react","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","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/discord.png","metadata":{"files":{"readme":"README.md","changelog":null,"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}},"created_at":"2020-04-27T01:24:57.000Z","updated_at":"2025-02-11T01:16:25.000Z","dependencies_parsed_at":"2024-02-04T19:44:00.903Z","dependency_job_id":null,"html_url":"https://github.com/discord/focus-layers","commit_stats":{"total_commits":58,"total_committers":6,"mean_commits":9.666666666666666,"dds":0.3448275862068966,"last_synced_commit":"fbffee80a3daee810f9acc286973d27f00ffff6f"},"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/discord%2Ffocus-layers","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/discord%2Ffocus-layers/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/discord%2Ffocus-layers/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/discord%2Ffocus-layers/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/discord","download_url":"https://codeload.github.com/discord/focus-layers/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247135141,"owners_count":20889420,"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","dialog","focus-lock","focus-trap","modals","react","typescript"],"created_at":"2024-08-03T01:03:41.929Z","updated_at":"2025-04-04T07:05:13.021Z","avatar_url":"https://github.com/discord.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# Focus Layers\n\nTiny React hooks for isolating focus within subsections of the DOM. Useful for supporting accessible\n`dialog` widgets like modals, popouts, and alerts.\n\nNo wrapper components, no emulation, no pre-defined \"list of tabbable elements\", and no data\nattributes. Implemented entirely with native APIs and events, with no additional dependencies.\n\n## Installation\n\nThis package is published under [`focus-layers`](https://www.npmjs.com/package/focus-layers) and can be installed with any npm-compatible package manager.\n\n## Basic Usage\n\nCall `useFocusLock` inside a component and provide it a ref to the DOM node to use as the container\nfor the focus layer. When the component mounts, it will lock focus to elements within that node, and\nthe lock will be released when the component unmounts.\n\n```tsx\nimport useFocusLock from \"focus-layers\";\n\nfunction Dialog({ children }: { children: React.ReactNode }) {\n  const containerRef = React.useRef\u003cHTMLElement\u003e();\n  useFocusLock(containerRef);\n\n  return (\n    \u003cdiv ref={containerRef} tabIndex={-1}\u003e\n      {children}\n    \u003c/div\u003e\n  );\n}\n\nfunction App() {\n  const [open, setOpen] = React.useState(false);\n\n  return (\n    \u003cdiv\u003e\n      \u003cbutton onClick={() =\u003e setOpen(true)}\u003eShow Dialog\u003c/button\u003e\n      {open \u0026\u0026 (\n        \u003cDialog\u003e\n          \u003cp\u003e\n            When this dialog is open, focus is trapped inside! Tabbing will always bring focus back\n            to this button.\n          \u003c/p\u003e\n          \u003cbutton onClick={() =\u003e setOpen(false)}\u003eClose Dialog\u003c/button\u003e\n        \u003c/Dialog\u003e\n      )}\n    \u003c/div\u003e\n  );\n}\n```\n\n## Returning Focus\n\nAfter unmounting, locks will return focus to the element that was focused when the lock was mounted.\nThis return target can also be controlled by the second parameter of `useFocusLock`.\n\n```tsx\nfunction DialogWithExplicitReturn() {\n  const [open, setOpen] = React.useState(false);\n\n  const containerRef = React.useRef\u003cHTMLDivElement\u003e();\n  const returnRef = React.useRef\u003cHTMLButtonElement\u003e();\n  useFocusLock(containerRef, { returnRef });\n\n  return (\n    \u003cReact.Fragment\u003e\n      \u003cbutton ref={returnRef}\u003eFocus will be returned here\u003c/button\u003e\n      \u003cbutton onClick={() =\u003e setOpen(true)}\u003eEven though this button opens the Dialog\u003c/button\u003e\n      {open \u0026\u0026 (\n        \u003cDialog\u003e\n          \u003cbutton onClick={() =\u003e setOpen(false)}\u003eClose Dialog\u003c/button\u003e\n        \u003c/Dialog\u003e\n      )}\n    \u003c/React.Fragment\u003e\n  );\n}\n```\n\n## Lock Layers\n\nLocks Layers are quite literally layers of locks, meaning they can be stacked on top of each other\nto create a chain of focus traps. When the top layer unmounts, the layer below it takes over as the\nactive lock. Layers can be removed in any order, and the top layer will always remain active.\n\nThis is useful for implementing confirmations inside of modals, or flows between multiple\nindependent modals, where one dialog will open another, and so on.\n\n```tsx\nfunction LayeredDialogs() {\n  const [firstOpen, setFirstOpen] = React.useState(false);\n  const [secondOpen, setSecondOpen] = React.useState(false);\n\n  return (\n    \u003cdiv\u003e\n      \u003cbutton onClick={() =\u003e setFirstOpen(true)}\u003eOpen First Dialog\u003c/button\u003e\n\n      {firstOpen \u0026\u0026 (\n        \u003cDialog\u003e\n          \u003cp\u003eThis is the first dialog that has a second confirmation after it.\u003c/p\u003e\n          \u003cbutton onClick={() =\u003e setSecondOpen(true)}\u003eConfirm\u003c/button\u003e\n        \u003c/Dialog\u003e\n      )}\n\n      {secondOpen \u0026\u0026 (\n        \u003cDialog\u003e\n          \u003cp\u003eThis is the second dialog, opened by the first one.\u003c/p\u003e\n          \u003cbutton onClick={() =\u003e setSecondOpen(false)}\u003eConfirm this dialog\u003c/button\u003e\n          \u003cbutton\n            onClick={() =\u003e {\n              setSecondOpen(false);\n              setFirstOpen(false);\n            }}\u003e\n            Close both dialogs\n          \u003c/button\u003e\n        \u003c/Dialog\u003e\n      )}\n    \u003c/div\u003e\n  );\n}\n```\n\nNote that layers only track their own return targets. If multiple layers are unmounting, it is not\nalways guaranteed that the original return target will be focused afterward. In this case, it is\nbest to provide an explicit return target so that focus is not left ambiguous after unmounting.\n\n## Subscribing to the Lock Stack\n\nLayers are managed by a global `LOCK_STACK` object. You can subscribe to this stack to get updates\nwhenever any focus layers are active. This is useful for marking the rest of your app with\n`aria-hidden` when modals are active, or performing any other tasks on demand:\n\n```tsx\nimport { LOCK_STACK } from \"focus-layers\";\n\nfunction App() {\n  const [focusLockActive, setFocusLockActive] = React.useState(false);\n  React.useEffect(() =\u003e {\n    LOCK_STACK.subscribe(setFocusLockActive);\n    return () =\u003e LOCK_STACK.unsubscribe(setFocusLockActive);\n  }, []);\n\n  return (\n    \u003cReact.Fragment\u003e\n      // This div represents your main app content\n      \u003cdiv aria-hidden={focusLockActive} /\u003e\n      // This div would be where the dialog layers are rendered\n      \u003cdiv /\u003e\n    \u003c/React.Fragment\u003e\n  );\n}\n```\n\nThe `subscribe` and `unsubscribe` methods are useful for listening to stack changes outside of the\nReact's rendering pipeline, but as a convenience, the `useLockSubscription` hook performs the same\nbehavior tied to a component's lifecycle.\n\n```tsx\nimport { useLockSubscription } from \"focus-layers\";\n\nfunction Component() {\n  useLockSubscription((enabled) =\u003e\n    console.log(`focus locking is now ${enabled ? \"enabled\" : \"disabled\"}`),\n  );\n}\n```\n\n## Edge Guards\n\nBrowsers do not provide a clean way of intercepting focus events that cause focus to leave the DOM.\nSpecifically, there is no way to directly prevent a tab/shift-tab action from moving focus out of\nthe document and onto the browser's controls or another window.\n\nThis can cause issues with focus isolation at the edges of the DOM, where there are no more tabbable\nelements past the focus lock layer for focus to move to before exiting the DOM.\n\nSemantically, this is valid behavior, but it is often nice to ensure that focus is still locked\nconsistently. The solution is to add hidden divs with `tabindex=\"0\"` to the beginning and end of the\nDOM (or around the focus layer) so that there is always another element for focus to move to while\ninside of a focus layer.\n\nThis library provides a `FocusGuard` component that you can render which will automatically activate\nwhen any focus layer is active, and hide itself otherwise. It renders a div that is always visually\nhidden, but becomes tabbable when active. These can be added at the actual edges of the DOM, or just\ndirectly surrounding any active focus layers.\n\n```tsx\nimport { FocusGuard } from \"focus-layers\";\n\nfunction App() {\n  return (\n    \u003cdiv id=\"app-root\"\u003e\n      \u003cFocusGuard /\u003e\n      // Render the rest of your app or your modal layers here.\n      \u003cFocusGuard /\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\nFocus will still be trapped even without these guards in place, but the user will be able to tab out\nof the page and onto their browser controls if the layer is at either the very beginning or very end\nof the document's tab order.\n\n## Distributed Focus Groups\n\nLayers with multiple, unconnected container nodes are not currently supported. This means layers\nthat have content and render a React Portal as part of the layer may not allow focus to leave the\nlayer and reach the portal.\n\nRendering a layer entirely within a Portal, or by any other means where there is a single containing\nnode, is supported.\n\nSupport for groups may be added in the future to address this issue. However, it's worth noting that\nPortals already do not play well with tab orders and should generally not be used as anything other\nthan an isolated focus layer. Otherwise the entire premise that focus is locked into the layer is\neffectively broken anyway.\n\n## External Focus Layers\n\nThe `LOCK_STACK` provides a way of integrating your own layers into the system. This can be useful\nwhen integrating with other libraries or components that implement their own focus management, or\nmanually triggering focus locks that aren't tied to a component lifecycle.\n\nActivating a layer is as simple as calling `add` with a `uid` and an `EnabledCallback`, which will\nbe called when the `LOCK_STACK` determines that the layer should be active. The callback will be\ninvoked immediately by the call to `add`, indicating that the layer is now active. The layer can\nthen be removed at any time in the future via the `remove` method.\n\n```typescript\nimport { LOCK_STACK } from \"focus-layers\";\n\nconst enabled = false;\nconst setEnabled = (now) =\u003e (enabled = now);\n\nLOCK_STACK.add(\"custom lock\", setEnabled);\n// Sometime later\nLOCK_STACK.remove(\"custom lock\");\n```\n\nIntegrating with the `LOCK_STACK` is a promise that your lock will enable and disable itself when it\nis told to (via the callback). Adding your lock to the stack is also a promise that you will remove\nit from the stack once the lock is \"unmounted\" or otherwise removed from use. Without removing your\nlock, all layers below your lock will be unable to regain focus.\n\nIf you are inside of a component and want to tie the focus lock to its lifecycle, you can instead\nuse the `useLockLayer` hook to simplify adding and removing. In return it provides a boolean\nindicating whether the lock is currently enabled, and will force a re-render when that state\nchanges:\n\n```tsx\nimport { useLockLayer } from \"focus-layers\";\n\nfunction Component() {\n  const enabled = useLockLayer();\n\n  React.useEffect(() =\u003e {\n    toggleCustomLock(enabled);\n  }, [enabled]);\n\n  return \u003cp\u003eCustom lock is {enabled ? \"enabled\" : \"disabled\"}\u003c/p\u003e;\n}\n```\n\n## Free Focus Layers\n\nCustom locks can also be used to implement \"free focus layers\" without losing the context of the\nfocus layers that are currently in place. Free focus is a situation where focus is not locked into\nany subsection and can move freely throughout the document. This can be useful for single-page\napplications that want to preserve focus state between multiple views where previous views get\nremoved from the DOM while another view takes its place.\n\nA free focus layer can easily be implemented as part of a Component. In the single-page application\nuse case mentioned above, this might happen in the base `View` component that wraps each view.\n\n```tsx\nimport { useLockLayer } from \"focus-layers\";\n\nfunction View() {\n  useLockLayer();\n\n  return \u003cdiv /\u003e;\n}\n```\n\nThe layer gets added on mount, disabling all layers below it, and since there's no new lock to\nactivate, the return value is just ignored, and nothing else needs to happen.\n\n## Scoped Roots\n\nBy default `useFocusLock` will attach a focus listener to the `document` itself, to capture all\nfocus events that happen on the page. Sometimes this can have unintended consequences where a\nutility function wants to quickly focus an external element and perform an action. For example,\ncross-platform copy utilities often do this\n([see MDN clipboard example](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Interact_with_the_clipboard#Writing_to_the_clipboard)).\n\nFree Focus layers _would_ be good for this, but because these hooks are based around `useEffect`,\nit's likely that the current lock layer wouldn't be disabled in time for the utility to do its job.\nTo get around this, you can scope where `useFocusLock` attaches listeners via the `attachTo` option.\nA good candidate for this is the node that your app is mounted to, and then have these other\nutilities do their work outside of that subtree.\n\n```tsx\nimport { useFocusLock } from \"focus-layers\";\n\nfunction DialogWithCopyableText() {\n  const containerRef = React.useRef\u003cHTMLDivElement\u003e();\n  useFocusLock(containerRef, { attachTo: document.getElementById(\"app-mount\") || document });\n\n  const text = \"this is the copied text\";\n\n  return (\n    \u003cdiv containerRef={containerRef}\u003e\n      \u003cbutton onClick={() =\u003e copy(text)}\u003eCopy some text\u003c/button\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n## Alternatives\n\nThis library was created after multiple attempts at using other focus locking libraries and wanting\nsomething with a simpler implementation that leverages the browser's APIs as much as possible, and\nfewer implications on DOM and Component structures. There are multiple other options out there that\nperform a similar job in different ways, such as:\n\n- [react-focus-lock](https://github.com/theKashey/react-focus-lock): Lots of options, very flexible,\n  but uses a lot of DOM nodes and attributes.\n- [react-focus-trap](https://github.com/vigetlabs/react-focus-trap): Nice and simple, but uses two\n  `div` containers and does not work nicely with `shift+tab` navigation.\n- [focus-trap-react](https://github.com/davidtheclark/focus-trap-react): Lots of useful features,\n  but relies on intercepting key and mouse events and querying for tabbable elements.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdiscord%2Ffocus-layers","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdiscord%2Ffocus-layers","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdiscord%2Ffocus-layers/lists"}