{"id":16347956,"url":"https://github.com/ai/keyux","last_synced_at":"2025-05-14T16:12:11.969Z","repository":{"id":222188055,"uuid":"756304355","full_name":"ai/keyux","owner":"ai","description":"JS library to improve keyboard UI of web apps","archived":false,"fork":false,"pushed_at":"2024-11-05T19:54:36.000Z","size":698,"stargazers_count":438,"open_issues_count":6,"forks_count":22,"subscribers_count":5,"default_branch":"main","last_synced_at":"2024-11-24T17:54:34.820Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://ai.github.io/keyux/","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/ai.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","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},"funding":{"github":"ai"}},"created_at":"2024-02-12T12:00:43.000Z","updated_at":"2024-11-20T14:22:04.000Z","dependencies_parsed_at":"2024-11-14T18:01:13.442Z","dependency_job_id":null,"html_url":"https://github.com/ai/keyux","commit_stats":{"total_commits":228,"total_committers":14,"mean_commits":"16.285714285714285","dds":"0.38596491228070173","last_synced_commit":"82e3ec0ba6b482abfd912e0d0083b016d80ce59b"},"previous_names":["ai/keyux"],"tags_count":21,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ai%2Fkeyux","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ai%2Fkeyux/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ai%2Fkeyux/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ai%2Fkeyux/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ai","download_url":"https://codeload.github.com/ai/keyux/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248688543,"owners_count":21145763,"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":[],"created_at":"2024-10-11T00:47:28.383Z","updated_at":"2025-04-13T08:54:32.528Z","avatar_url":"https://github.com/ai.png","language":"TypeScript","funding_links":["https://github.com/sponsors/ai"],"categories":[],"sub_categories":[],"readme":"# KeyUX\n\nJS library to improve the keyboard UI of web apps. It is designed not only\nfor **a11y**, but also to create **professions tools** where users prefer\nto use the keyboard.\n\n* Add **hotkeys** with `aria-keyshortcuts`.\n* Show a button’s `:active` state when a hotkey is pressed.\n* Enable **navigation with keyboard arrows** in `role=\"menu\"` lists.\n* Jump to the next section according to `aria-controls` and back\n  with \u003ckbd\u003eEsc\u003c/kbd\u003e.\n* Show and hide submenus of `role=\"menu\"`.\n* Allow users to **override hotkeys**.\n* **2 KB** (minified and brotlied). No dependencies.\n* Vanilla JS and works with any framework including React, Vue, Svelte.\n\n```jsx\nexport const Button = ({ hotkey, children }) =\u003e {\n  return \u003cbutton aria-keyshortcuts={hotkey}\u003e\n    {children}\n    {likelyWithKeyboard(window) \u0026\u0026 \u003ckbd\u003e{getHotKeyHint(window, hotkey)}\u003c/kbd\u003e}\n  \u003c/button\u003e\n}\n```\n\nSee [demo page](https://ai.github.io/keyux/)\nand [example](./test/demo/index.tsx):\n\nhttps://github.com/user-attachments/assets/bcd78271-cf76-45a3-8beb-4f3cea69c143\n\n---\n\n- [Install](#install)\n- [Hotkeys](#hotkeys)\n  - [Hotkeys Hint](#hotkeys-hint)\n  - [Pressed State](#pressed-state)\n  - [Hotkeys Override](#hotkeys-override)\n  - [Hotkeys for List](#hotkeys-for-list)\n  - [Meta instead of Ctrl on Mac](#meta-instead-of-ctrl-on-mac)\n- [Focus Groups](#focus-groups)\n  - [`focusgroup` attribute](#focusgroup-attribute)\n  - [Menu](#menu)\n  - [Listbox](#listbox)\n  - [Tablist](#tablist)\n  - [Toolbar](#toolbar)\n- [Focus Jumps](#focus-jumps)\n  - [Nested Menu](#nested-menu)\n\n---\n\n\u003cimg src=\"https://cdn.evilmartians.com/badges/logo-no-label.svg\" alt=\"\" width=\"22\" height=\"16\" /\u003e  Made at \u003cb\u003e\u003ca href=\"https://evilmartians.com/devtools?utm_source=keyux\u0026utm_campaign=devtools-button\u0026utm_medium=github\"\u003eEvil Martians\u003c/a\u003e\u003c/b\u003e, product consulting for \u003cb\u003edeveloper tools\u003c/b\u003e.\n\n---\n\n\n## Install\n\n```sh\nnpm install keyux\n```\n\nThen add the `startKeyUX` call with the necessary features to the main JS file.\n\n```js\nimport {\n  hiddenKeyUX,\n  hotkeyKeyUX,\n  hotkeyOverrides,\n  jumpKeyUX,\n  focusGroupKeyUX,\n  focusGroupPolyfill,\n  pressKeyUX,\n  startKeyUX\n} from 'keyux'\n\nconst overrides = hotkeyOverrides({})\n\nstartKeyUX(window, [\n  hotkeyKeyUX([overrides]),\n  focusGroupKeyUX(),\n  focusGroupPolyfill(),\n  pressKeyUX('is-pressed'),\n  jumpKeyUX(),\n  hiddenKeyUX()\n])\n```\n\n\n## Hotkeys\n\nWhen the user presses a hotkey, KeyUX will click on the button or link\nwith the same hotkey in `aria-keyshortcuts`.\n\nFor instance, KeyUX will click on this button if user press\n\u003ckbd\u003eAlt\u003c/kbd\u003e+\u003ckbd\u003eB\u003c/kbd\u003e or \u003ckbd\u003e⌥\u003c/kbd\u003e \u003ckbd\u003eB\u003c/kbd\u003e.\n\n```jsx\n\u003cbutton aria-keyshortcuts=\"alt+b\"\u003eBold\u003c/button\u003e\n```\n\nYou can use hotkey to move focus to text input or textarea:\n\n```jsx\n\u003cinput type=\"search\" aria-keyshortcuts=\"s\" placeholder=\"S\" /\u003e\n```\n\nThe hotkey pattern should contain modifiers like `meta+ctrl+alt+shift+b`\nin this exact order.\n\nTo enable this feature, call `hotkeyKeyUX`:\n\n```js\nimport { hotkeyKeyUX, startKeyUX } from 'keyux'\n\nstartKeyUX(window, [\n  hotkeyKeyUX()\n])\n```\n\nHotkeys inside block with `inert` or `aria-hidden` attribute will be ignored.\nYou can use it, to disable page’s hotkeys when dialog is shown:\n\n```html\n\u003cmain inert\u003e\n  \u003cbutton aria-keyshortcuts=\"h\"\u003eHelp\u003c/button\u003e \u003c!-- Will be ignored --\u003e\n\u003c/main\u003e\n\u003cdialog\u003e\n  …\n\u003c/dialog\u003e\n```\n\n\n### Hotkeys Hint\n\nYou can render the hotkey hint from the `aria-keyshortcuts` pattern in\na prettier way:\n\n```jsx\nimport { likelyWithKeyboard, getHotKeyHint } from 'keyux'\n\nexport const Button = ({ hokey, children }) =\u003e {\n  return \u003cbutton aria-keyshortcuts={hotkey}\u003e\n    {children}\n    {likelyWithKeyboard(window) \u0026\u0026 \u003ckbd\u003e{getHotKeyHint(window, hotkey)}\u003c/kbd\u003e}\n  \u003c/button\u003e\n}\n```\n\n`likelyWithKeyboard()` returns `false` on mobile devices where user is unlikely\nto be able to use hotkeys (but it is still possible by connecting an\nexternal keyboard).\n\n`getHotKeyHint()` replaces modifiers for Mac and makes text prettier.\nFor instance, for `alt+b` it will return `Alt + B` on Windows/Linux or `⌥ B`\non Mac.\n\nIf you’re using overrides, pass the same override config both to `hotkeyKeyUX()`\nand `getHotKeyHint()` for accurate hints:\n\n```js\nimport {\n  getHotKeyHint,\n  hotkeyOverrides,\n  hotkeyKeyUX,\n  startKeyUX\n} from 'keyux'\n\nlet config = { 'alt+b': 'b' }\n\nstartKeyUX(window, [\n  hotkeyKeyUX([hotkeyOverrides(config)]) // Override B to Alt + B\n])\ngetHotKeyHint(window, 'b', [hotkeyOverrides(config)]) // Alt + B\n```\n\nOne-letter hotkeys (like \u003ckbd\u003eB\u003c/kbd\u003e) will be ignored if user’s focus is inside\ntext inputs or [focus groups](#focus-groups). This is why for general hotkeys\nwe recommend add some modifier like \u003ckbd\u003eAlt\u003c/kbd\u003e+\u003ckbd\u003eB\u003c/kbd\u003e.\n\n\n### Pressed State\n\nKeyUX can set class to show pressed state for a button when user\npresses a hotkey. It will make the UI more responsive.\n\n```js\nimport { hotkeyKeyUX, startKeyUX, pressKeyUX } from 'keyux'\n\nstartKeyUX(window, [\n  pressKeyUX('is-pressed'),\n  hotkeyKeyUX()\n])\n```\n\n```css\nbutton {\n  \u0026:active,\n  \u0026.is-pressed {\n    transform: scale(0.95);\n  }\n}\n```\noverriding\nYou can use\n[`postcss-pseudo-classes`](https://github.com/giuseppeg/postcss-pseudo-classes)\nto automatically add class for every `:active` state in your CSS.\n\n\n### Hotkeys Override\n\nMany users want to override hotkeys because your hotkeys can conflict with\ntheir browser’s extensions, system, or screen reader.\n\nKeyUX allows overriding hotkeys using tranforms. Use the `hotkeyOverrides()`\ntranformer with `hotkeyKeyUX()` and `getHotKeyHint()`.\n\nYou will need to create some UI for users to fill this object like:\n\n```js\nconst overrides = {\n  'alt+b': 'b' // Override B to Alt + B\n}\n```\n\nThen KeyUX will click on `aria-keyshortcuts=\"b\"` when\n\u003ckbd\u003eAlt\u003c/kbd\u003e+\u003ckbd\u003eB\u003c/kbd\u003e is pressed, and\n`getHotKeyHint(window, 'b', [hotkeyOverrides(overrides)])` will return\n`Alt + B`/`⌥ B`.\n\n\n### Hotkeys for List\n\nWebsites may have hotkeys for each list element. For instance, for “Add to card”\nbutton in shopping list.\n\nTo implement it:\n1. Hide list item’s buttons by `data-keyux-ignore-hotkeys` from global search.\n2. Make list item focusable by `tabindex=\"0\"`. When item has a focus,\n   KeyUX ignores `data-keyux-ignore-hotkeys`.\n\n```jsx\n\u003cli data-keyux-ignore-hotkeys tabIndex={0}\u003e\n  {product.title}\n  \u003cbutton aria-keyshortcuts=\"a\" tabIndex={-1}\u003eAdd to card\u003c/button\u003e\n\u003c/li\u003e\n```\n\nIf you have common panel with actions for focused item, you can use\n`data-keyux-hotkeys` with ID of item’s panel.\n\n```js\n\u003cul\u003e\n  {products.map(product =\u003e {\n    return \u003cli data-keyux-hotkeys=\"panel\" tabIndex={0} key={product.id}\u003e\n      {product.title}\n    \u003c/li\u003e\n  })}\n\u003c/ul\u003e\n\u003cdiv id=\"panel\" data-keyux-ignore-hotkeys\u003e\n  \u003cbutton aria-keyshortcuts=\"a\" tabIndex={-1}\u003eAdd to card\u003c/button\u003e\n\u003c/div\u003e\n```\n\n\n### Meta instead of Ctrl on Mac\n\nIt’s common to use the \u003ckbd\u003eMeta\u003c/kbd\u003e (or \u003ckbd\u003e⌘\u003c/kbd\u003e) modifier for hotkeys\non Mac, while Windows and Linux usually favor the \u003ckbd\u003eCtrl\u003c/kbd\u003e key. To provide\nfamiliar experience on all platforms, enable the Mac compatibility transform:\n\n```js\nimport {\n  hotkeyMacCompat,\n  hotkeyKeyUX,\n  startKeyUX,\n  getHotKeyHint\n} from 'keyux'\n\nconst mac = hotkeyMacCompat();\nstartKeyUX(window, [hotkeyKeyUX([mac])])\ngetHotKeyHint(window, 'ctrl+b', [mac]) // Ctrl+B on Windows/Linux and ⌘+b on Mac\n```\n\nHotkeys pressed with the \u003ckbd\u003eMeta\u003c/kbd\u003e modifier will work as if\nthe \u003ckbd\u003eCtrl\u003c/kbd\u003e modifier was pressed.\n\n\n## Focus Groups\n\nUsing only \u003ckbd\u003eTab\u003c/kbd\u003e for navigation is not very useful. User may need to\npress it too many times to get to their button (also non-screen-reader users\ndon’t have quick navigation).\n\n\n### `focusgroup` attribute\n\nKey UX has limited polyfill for [`focusgroup` attribute](https://open-ui.org/components/focusgroup.explainer/) to mark groups where user will move `:focus`\nby arrows.\n\n```html\n\u003cdiv focusgroup\u003e\n  \u003cbutton type=\"button\"\u003eCopy\u003c/button\u003e\n  \u003cbutton type=\"button\"\u003ePaste\u003c/button\u003e\n  \u003cbutton type=\"button\"\u003eCut\u003c/button\u003e\n\u003c/div\u003e\n```\n\nKey UX supports (you can combine these features):\n- `focusgroup=\"block\"` for vertical arrows.\n- `focusgroup=\"no-memory\"` to not restore last focus position.\n- `focusgroup=\"wrap\"` enables cyclic focus movement within a group.\n- `focusgroup=\"none\"` excludes element from arrow key navigation.\n\nKey UX doesn’t support `grid` feature.\n\nTo enable this feature, call `focusGroupPolyfill`.\n\n```js\nimport { focusGroupPolyfill } from 'keyux'\n\nstartKeyUX(window, [\n  focusGroupPolyfill()\n])\n```\n\n### Menu\n\nTo reduce Tab-list you can group website’s menu\ninto [`role=\"menu\"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/menu_role)\nwith arrow navigation.\n\n```html\n\u003cnav role=\"menu\"\u003e\n  \u003ca href=\"/\" role=\"menuitem\"\u003eHome\u003c/a\u003e\n  \u003ca href=\"/catalog\" role=\"menuitem\"\u003eCatalog\u003c/a\u003e\n  \u003ca href=\"/contacts\" role=\"menuitem\"\u003eContacts\u003c/a\u003e\n\u003c/nav\u003e\n```\n\nUsers will use \u003ckbd\u003eTab\u003c/kbd\u003e to get inside the menu, and will use either\narrows or \u003ckbd\u003eHome\u003c/kbd\u003e,\n\u003ckbd\u003eEnd\u003c/kbd\u003e or an item name to navigate inside. User can search the menu item\nby typing the first characters of the item text.\n\nTo enable this feature, call `focusGroupKeyUX`.\n\n```js\nimport { focusGroupKeyUX } from 'keyux'\n\nstartKeyUX(window, [\n  focusGroupKeyUX()\n])\n```\n\n\n### Listbox\n\nThe [`role=\"listbox\"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role)\nis used for lists from which a user may select one or\nmore items which are static and, unlike HTML `\u003cselect\u003e` elements,\nmay contain images.\n\n```html\n\u003cul role=\"listbox\"\u003e\n  \u003cli tabindex=\"0\" role=\"option\"\u003ePizza\u003c/li\u003e\n  \u003cli tabindex=\"0\" role=\"option\"\u003eSushi\u003c/li\u003e\n  \u003cli tabindex=\"0\" role=\"option\"\u003eRamen\u003c/li\u003e\n\u003c/ul\u003e\n```\n\nUsers will use \u003ckbd\u003eTab\u003c/kbd\u003e to get inside the listbox, and will use either\narrows or \u003ckbd\u003eHome\u003c/kbd\u003e,\n\u003ckbd\u003eEnd\u003c/kbd\u003e or an item name to navigate inside.\n\nTo enable this feature, call `focusGroupKeyUX`.\n\n```js\nimport { focusGroupKeyUX } from 'keyux'\n\nstartKeyUX(window, [\n  focusGroupKeyUX()\n])\n```\n\n\n### Tablist\n\nThe [`role=\"tablist\"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role)\nidentifies the element that serves as the container for a set of tabs.\nThe tab content should be marked by `[role=\"tabpanel']`.\n\n```html\n\u003cdiv role=\"tablist\"\u003e\n  \u003cbutton role=\"tab\"\u003eHome\u003c/button\u003e\n  \u003cbutton role=\"tab\"\u003eAbout\u003c/button\u003e\n  \u003cbutton role=\"tab\"\u003eContact\u003c/button\u003e\n\u003c/div\u003e\n```\n\nUsers will use \u003ckbd\u003eTab\u003c/kbd\u003e to get inside the tablist, and will use either\narrows or \u003ckbd\u003eHome\u003c/kbd\u003e,\n\u003ckbd\u003eEnd\u003c/kbd\u003e.\n\nTo enable this feature, call `focusGroupKeyUX`.\n\n```js\nimport { focusGroupKeyUX } from 'keyux'\n\nstartKeyUX(window, [\n  focusGroupKeyUX()\n])\n```\n\n\n### Toolbar\n\nThe [`role=\"toolbar\"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/toolbar_role)\ndefines the containing element as a collection of commonly used function buttons\nor controls represented in a compact visual forms. Buttons inside the `toolbar`\nmust have `type=\"button\"` attribute because the default one is `submit`.\n\n```html\n\u003cdiv role=\"toolbar\"\u003e\n  \u003cdiv\u003e\n    \u003cbutton type=\"button\"\u003eCopy\u003c/button\u003e\n    \u003cbutton type=\"button\"\u003ePaste\u003c/button\u003e\n    \u003cbutton type=\"button\"\u003eCut\u003c/button\u003e\n  \u003c/div\u003e\n  \u003cdiv\u003e\n    \u003cinput type=\"checkbox\" /\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n```\n\nUsers will use \u003ckbd\u003eTab\u003c/kbd\u003e to get inside the tablist, and will use either\narrows or \u003ckbd\u003eHome\u003c/kbd\u003e,\n\u003ckbd\u003eEnd\u003c/kbd\u003e.\n\nTo enable this feature, call `focusGroupKeyUX`.\n\n```js\nimport { focusGroupKeyUX } from 'keyux'\n\nstartKeyUX(window, [\n  focusGroupKeyUX()\n])\n```\n\n## Focus Jumps\n\nAfter finishing in one section, you can move user’s focus to the next step\nto save time. For example, you can move the cursor to the page after the user\nselects it from the menu. Or, you can move the focus to the item’s form after\nthe user selects an item in the list.\n\nYou can control where the focus moves next with `aria-controls`.\n\n```jsx\n\u003cdiv role=\"menu\"\u003e\n  {products.map(({ id, name }) =\u003e\n    \u003cbutton role=\"menuitem\" aria-controls=\"product_form\"\u003e{name}\u003c/button\u003e\n  )}\n\u003c/div\u003e\n\n\u003cdiv id=\"product_form\"\u003e\n  …\n\u003c/div\u003e\n```\n\nOn \u003ckbd\u003eEsc\u003c/kbd\u003e the focus will jump back.\n\nYou can add `aria-controls` to `\u003cinput\u003e` to make the focus jump\non \u003ckbd\u003eEnter\u003c/kbd\u003e.\n\n```html\n\u003cinput type=\"search\" aria-controls=\"search_results\" /\u003e\n```\n\nTo enable this feature, call `jumpKeyUX`.\n\n```js\nimport { focusGroupKeyUX, jumpKeyUX } from 'keyux'\n\nstartKeyUX(window, [\n  focusGroupKeyUX(),\n  jumpKeyUX()\n])\n```\n\n\n### Nested Menu\n\nYou can make nested menus with KeyUX with `aria-controls`\nand `aria-hidden=\"true\"`.\n\n```html\n\u003cbutton aria-controls=\"edit\" aria-haspopup=\"menu\"\u003eEdit\u003c/button\u003e\n\n\u003cdiv id=\"edit\" hidden aria-hidden=\"true\" role=\"menu\"\u003e\n  \u003cbutton role=\"menuitem\"\u003eUndo\u003c/button\u003e\n  \u003cbutton role=\"menuitem\" aria-controls=\"find\" aria-haspopup=\"menu\"\u003eFind\u003c/button\u003e\n\u003c/div\u003e\n\n\u003cdiv id=\"find\" hidden aria-hidden=\"true\" role=\"menu\"\u003e\n  \u003cbutton role=\"menuitem\"\u003eFind…\u003c/button\u003e\n  \u003cbutton role=\"menuitem\"\u003eReplace…\u003c/button\u003e\n\u003c/div\u003e\n```\n\nYou can make the nested menu visible by diabling `hidden`, but you will\nhave to set `tabindex=\"-1\"` manually.\n\nTo enable this feature, call `hiddenKeyUX`.\n\n```js\nimport { focusGroupKeyUX, jumpKeyUX, hiddenKeyUX } from 'keyux'\n\nstartKeyUX(window, [\n  focusGroupKeyUX(),\n  jumpKeyUX(),\n  hiddenKeyUX()\n])\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fai%2Fkeyux","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fai%2Fkeyux","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fai%2Fkeyux/lists"}