JS library to improve the keyboard UI of web apps. It is designed not only for a11y, but also to create professions tools where users prefer to use the keyboard.
- Add hotkeys with
aria-keyshortcuts. - Show a button’s
:activestate when a hotkey is pressed. - Enable navigation with keyboard arrows in
role="menu"lists. - Jump to the next section according to
aria-controlsand back with Esc. - Show and hide submenus of
role="menu". - Allow users to override hotkeys.
- 2 KB (minified and brotlied). No dependencies.
- Vanilla JS and works with any framework including React, Vue, Svelte.
export const Button = ({ hotkey, children }) => {
return <button aria-keyshortcuts={hotkey}>
{children}
{likelyWithKeyboard(window) && <kbd>{getHotKeyHint(window, hotkey)}</kbd>}
</button>
} Made in Evil Martians, product consulting for developer tools.
npm install keyuxThen add the startKeyUX call with the necessary features to the main JS file.
import {
hiddenKeyUX,
hotkeyKeyUX,
jumpKeyUX,
menuKeyUX,
pressKeyUX,
startKeyUX
} from 'keyux'
const overrides = {}
startKeyUX(window, [
hotkeyKeyUX(overrides),
menuKeyUX(),
pressKeyUX('is-pressed'),
jumpKeyUX(),
hiddenKeyUX()
])When the user presses a hotkey, KeyUX will click on the button or link
with the same hotkey in aria-keyshortcuts.
For instance, KeyUX will click on this button if user press Alt+B or ⌥ B.
<button aria-keyshortcuts="alt+b">Bold</button>The hotkey pattern should contain modifiers like meta+ctrl+alt+shift+b in this exact order.
To enable this feature, call hotkeyKeyUX:
import { hotkeyKeyUX, startKeyUX } from 'keyux'
startKeyUX(window, [
hotkeyKeyUX()
])You can render the hotkey hint from the aria-keyshortcuts pattern in a prettier way:
import { likelyWithKeyboard, getHotKeyHint } from 'keyux'
export const Button = ({ hokey, children }) => {
return <button aria-keyshortcuts={hotkey}>
{children}
{likelyWithKeyboard(window) && <kbd>{getHotKeyHint(window, hotkey)}</kbd>}
</button>
}likelyWithKeyboard() returns false on mobile devices where user are unlikely
to be able to use hotkeys (but it is still possible by connecting an
external keyboard).
getHotKeyHint() replaces modifiers for Mac and makes text prettier.
For instance, for alt+b it will return Alt + B on Windows/Linux or ⌥ B
on Mac.
KeyUX can set class to show pressed state for a button when user presses a hotkey. It will make the UI more responsive.
import { hotkeyKeyUX, startKeyUX, pressKeyUX } from 'keyux'
startKeyUX(window, [
pressKeyUX('is-pressed'),
hotkeyKeyUX()
])button {
&:active,
&.is-pressed {
transform: scale(0.95);
}
}You can use
postcss-pseudo-classes
to automatically add class for every :active state in your CSS.
Many users want to override hotkeys because your hotkeys can conflict with their browser’s extensions, system, or screen reader.
KeyUX allows to override hotkeys using the overrides object. Both hotkeyKeyUX()
and getHotKeyHint() accept it as an argument.
You will need to create some UI for users to fill this object like:
const overrides = {
'alt+b': 'b' // Override B to Alt + B
}Then KeyUX will click on aria-keyshortcuts="b" when Alt+B
is pressed, and getHotKeyHint(window, 'b', overrides) will return Alt + B/⌥ B.
Websites may have hotkeys for each list element. For instance, for “Add to card” button in shopping list.
To implement it:
- Hide list item’s buttons by
data-keyux-ignore-hotkeysfrom global search. - Make list item focusable by
tabindex="0". When item has a focus, KeyUX ignoresdata-keyux-ignore-hotkeys.
<li data-keyux-ignore-hotkeys tabindex="0">
{product.title}
<button aria-keyshortcuts="a">Add to card</button>
</li>If you have common panel with actions for focused item, you can use
data-keyux-hotkeys with ID of item’s panel.
<ul>
{products.map(product => {
return <li data-keyux-hotkeys="panel" tabindex="0" key={product.id}>
{product.title}
</li>
})}
</ul>
<div id="panel" data-keyux-ignore-hotkeys>
<button aria-keyshortcuts="a">Add to card</button>
</div>Using only Tab for navigation is not very useful. User may need to press it too many times to get to their button (also non-screen-reader users don’t have quick navigation).
To reduce Tab-list you can group some related things (tabs or website’s menu)
into role="menu"
with arrow navigation.
<nav role="menu">
<a href="/" role="menuitem">Home</a>
<a href="/catalog" role="menuitem">Catalog</a>
<a href="/contacts" role="menuitem">Contacts</a>
</nav>Users will use Tab to get inside the menu, and will use either arrows or Home, End or an item name to navigate inside.
To enable this feature, call menuKeyUX.
import { menuKeyUX } from 'keyux'
startKeyUX(window, [
menuKeyUX()
])After finishing in one section, you can move user’s focus to the next step to save time. For example, you can move the cursor to the page after the user selects it from the menu. Or, you can move the focus to the item’s form after the user selects an item in the list.
You can control where the focus moves next with aria-controls.
<div role="menu">
{products.map(({ id, name }) =>
<button role="menuitem" aria-controls="product_form">{name}</button>
)}
</div>
<div id="product_form">
…
</div>On Esc the focus will jump back.
You can add aria-controls to <input> to make the focus jump on Enter.
<input type="search" aria-controls="search_results" />To enable this feature, call jumpKeyUX.
import { menuKeyUX, jumpKeyUX } from 'keyux'
startKeyUX(window, [
menuKeyUX(),
jumpKeyUX()
])You can make nested menus with KeyUX with aria-controls
and aria-hidden="true".
<button aria-controls="edit" aria-haspopup="menu">Edit</button>
<div id="edit" hidden aria-hidden="true" role="menu">
<button role="menuitem">Undo</button>
<button role="menuitem" aria-controls="find" aria-haspopup="menu">Find</button>
</div>
<div id="find" hidden aria-hidden="true" role="menu">
<button role="menuitem">Find…</button>
<button role="menuitem">Replace…</button>
</div>You can make the nested menu visible by diabling hidden, but you will
have to set tabindex="-1" manually.
To enable this feature, call hiddenKeyUX.
import { menuKeyUX, jumpKeyUX, hiddenKeyUX } from 'keyux'
startKeyUX(window, [
menuKeyUX(),
jumpKeyUX(),
hiddenKeyUX()
])