{"id":37459105,"url":"https://github.com/dividat/focus-shift","last_synced_at":"2026-01-20T17:46:41.817Z","repository":{"id":221841262,"uuid":"747067550","full_name":"dividat/focus-shift","owner":"dividat","description":"A simple library for moving focus around a page using the arrow keys","archived":false,"fork":false,"pushed_at":"2025-10-16T15:34:09.000Z","size":95,"stargazers_count":0,"open_issues_count":1,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-17T18:13:06.772Z","etag":null,"topics":["navigation","spatial-navigation","vanilla-js"],"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/dividat.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,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-01-23T07:43:54.000Z","updated_at":"2025-10-16T15:34:14.000Z","dependencies_parsed_at":"2024-03-17T15:26:09.961Z","dependency_job_id":"d8fd8cb4-abad-4006-8cbd-8720aa915107","html_url":"https://github.com/dividat/focus-shift","commit_stats":null,"previous_names":["dividat/focus-shift"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/dividat/focus-shift","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dividat%2Ffocus-shift","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dividat%2Ffocus-shift/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dividat%2Ffocus-shift/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dividat%2Ffocus-shift/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dividat","download_url":"https://codeload.github.com/dividat/focus-shift/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dividat%2Ffocus-shift/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28477996,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-16T06:30:42.265Z","status":"ssl_error","status_checked_at":"2026-01-16T06:30:16.248Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["navigation","spatial-navigation","vanilla-js"],"created_at":"2026-01-16T07:01:44.505Z","updated_at":"2026-01-16T07:01:44.920Z","avatar_url":"https://github.com/dividat.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# focus-shift\n\nfocus-shift is a lightweight, zero-dependency JavaScript library designed for keyboard-based navigation in web applications. It restricts itself to shifting focus between elements in response to arrow key events. The behavior of focus shifting can be guided by annotations in the HTML markup. This allows the library to work well with technologies that prefer generating HTML over interacting with JavaScript directly.\n\n## Features\n\n- Move focus with the arrow keys\n- Declare groups with custom focus strategies\n- Mark subtrees of the DOM that should trap focus\n- Mark subtrees of the DOM that should be skipped\n- Mark elements to not trigger scrolling when receiving focus\n- Dispatches events which allow responding to or controlling focus shifts\n\n## Usage\n\nHere's a simple example of annotating markup:\n\n```html\n\u003cdiv data-focus-group=\"active\"\u003e\n  \u003cbutton\u003eHome\u003c/button\u003e\n  \u003cbutton data-focus-active\u003eAbout\u003c/button\u003e\n  \u003cbutton\u003eContact\u003c/button\u003e\n\u003c/div\u003e\n\n\u003cbutton data-focus-skip\u003eDelete your account\u003c/button\u003e\n\n\u003cscript type=\"module\" src=\"focus-shift.js\"\u003e\u003c/script\u003e\n```\n\n## Options\n\nThe following attributes may be added in the markup to guide the moving of focus:\n\n- `data-focus-group`: Defines a navigation group and the initial focus when focus moves to a group. Default is `linear`.\n  - `first`: The first element in the DOM order receives focus.\n  - `last`: The last element in the DOM order is focused initially.\n  - `active`: Focuses on the element within the group marked as active.\n  - `linear`: Focus is determined by the spatial direction of user navigation.\n  - `memorize`: The last focused element within the group receives focus again.\n- `data-focus-active`: Marks an element as the currently active element within a group.\n- `data-focus-skip`: Skips the element and its descendants in navigation.\n- `data-focus-trap`: Only allows elements within the annotated layer to receive focus.\n- `data-focus-prevent-scroll`: Prevents scrolling when the element receives focus.\n\nSetting `window.FOCUS_SHIFT_DEBUG = true` lets the library log processing steps to the browser's console.\n\n### CSS Options\n\nSome of focus-shift's behavior may be controlled using CSS, because it propagates nicely through the DOM tree, while allowing for overrides on individual elements or entire subtrees.\n\n- `--focus-interaction-behavior` determines behavior when arrow keys are pressed within input and textarea elements.\n  - `normal`: The default behavior of the browser will be preserved as much as possible. May come at the cost of input elements blocking spatial navigation.\n  - `opaque`: Input elements will be treated like other elements and focus will be shifted in the same way. Suitable for limited input devices (e.g. on-screen keyboards) and avoids ambiguities in interpreting arrow keys.\n\n### Events\n\nSome events may bubble up from the active element:\n\n- `focus-shift:initiate`: Dispatched before interpreting a spatial navigation gesture from the user. Prevent default to stop focus-shift from initiating spatial navigation. The event's detail contains the triggering `keyboardEvent`.\n- `focus-shift:exhausted`: Dispatched when focus-shift attempted to handle a user gesture but found no focus candidates along the requested direction. The event's detail contains the triggering `keyboardEvent` and requested `direction`.\n\n## Principles and Scope\n\n- **It doesn't just work.** It would be nice if focus could automatically move to the intuitive element in each case, but this seems to require a sophisticated model of visual weight and Gestalt principles. This is out of scope for a simple library like this.\n- **It should be easy to make it work.** With a little bit of annotation in the markup, one can express relationships to help the algorithm move focus in an adequate way.\n- **Annotations should be logical, not spatial.** To be useful in responsive layouts, the annotations should express logical rather than spatial relationships.\n- **Keep state to a minimum.** As much as possible, the library should treat each event in isolation and not maintain state representing the page layout. This may make the library less performant, but avoids complicated and error prone recomputation logic.\n\n### What the library doesn't do, but might\n\n- Dispatch cancelable events when descending into or out of groups\n- Treat elements in open shadow DOM as focusable\n- Allow defining custom selectors for focusables\n- Use focus heuristics based on user agent's [text direction](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/dir)\n- Offer a JavaScript API\n\n### What the library probably shouldn't do\n\n- Handle keyboard events other than arrow keys\n\n### What the library can not do\n\n- Handle focus in iframes or closed shadow DOM\n\n### Mechanism\n\n```mermaid\nflowchart TB\n    Idle(((Idle))) == keypress ==\u003e D_KP{Is arrow key?}\n    %% Terminology from https://github.com/whatwg/html/issues/897\n    D_KP -- Yes --\u003e A_BC[Get top blocking element]\n    D_KP -- No --\u003e Idle\n    A_BC --\u003e D_AE{Contains activeElement?}\n    D_AE -- No --\u003e A_SI[Select initial focus]\n    A_SI --\u003e Idle\n    D_AE -- Yes --\u003e Find\n\n    subgraph Find\n        direction TB\n        A_FC[Find candidates within next parent group]\n        A_FC --\u003e D_CF{Candidates found?}\n        D_CF -- Yes --\u003e A_SN[Stop with candidates]\n        D_CF -- No --\u003e D_PB[Is parent the top blocking element?]\n        D_PB -- Yes --\u003e A_NC[Stop with no candidates]\n        D_PB -- No --\u003e A_FC\n    end\n\n    Find --\u003e D_HC[One or more candidates?]\n    D_HC -- Yes --\u003e Activate\n    D_HC -- No --\u003e Idle\n\n    subgraph Activate\n        direction TB\n        D_DP[Direct projection along movement axis non-empty?] -- Yes --\u003e A_FD[Reduce to only those candidates]\n        D_DP -- No --\u003e A_CA[Continue with all candidates]\n        A_CA --\u003e A_SC[Select candidate with lowest Euclidean distance]\n        A_FD --\u003e A_SC\n        A_SC --\u003e D_CG[Is selected candidate a group?]\n        D_CG -- Yes --\u003e A_DG[Select new candidate based on group's strategy]\n        A_DG --\u003e D_CG\n        D_CG -- No --\u003e A_FS[Focus selected candidate]\n    end\n\n    Activate --\u003e Idle\n```\n\n## Development\n\nThe library is implemented in [withered](https://en.wikipedia.org/wiki/Gunpei_Yokoi#Lateral_Thinking_with_Withered_Technology) JavaScript, so it should work directly with most browsers and a development server is not needed.\n\nThere is ample JSDoc documentation so that the TypeScript compiler may be used for typechecking in strict mode:\n\n    npm test\n\nThe code is formatted with slightly non-standard prettier:\n\n    npm run format\n\nEnd-to-end tests are done using Cypress.\n\n## Contributing\n\nContributions are welcome. Please fork the repository and submit a pull request with your proposed changes.\n\n## Related Work and Inspiration\n\n- https://github.com/bbc/lrud-spatial, a nice and simple but more spatially oriented library\n- https://github.com/luke-chang/js-spatial-navigation, spatial navigation library with good functionality but JavaScript-focused and stateful configuration\n- https://github.com/WICG/spatial-navigation, a possibly abandoned proposal for a Web Platform API\n\n## License\n\n(C) Copyright 2024 Dividat AG\n\nPublished under the MIT License. See [LICENSE](LICENSE) for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdividat%2Ffocus-shift","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdividat%2Ffocus-shift","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdividat%2Ffocus-shift/lists"}