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
:active
state when a hotkey is pressed. - Enable navigation with keyboard arrows in
role="menu"
lists. - Jump to the next section according to
aria-controls
and 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>
}
keyux_demo.mp4
Made at Evil Martians, product consulting for developer tools.
npm install keyux
Then add the startKeyUX
call with the necessary features to the main JS file.
import {
hiddenKeyUX,
hotkeyKeyUX,
hotkeyOverrides,
jumpKeyUX,
focusGroupKeyUX,
pressKeyUX,
startKeyUX
} from 'keyux'
const overrides = hotkeyOverrides({})
startKeyUX(window, [
hotkeyKeyUX([overrides]),
focusGroupKeyUX(),
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>
You can use hotkey to move focus to text input or textarea:
<input type="search" aria-keyshortcuts="s" placeholder="S" />
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()
])
Hotkeys inside block with inert
or aria-hidden
attribute will be ignored.
You can use it, to disable page’s hotkeys when dialog is shown:
<main inert>
<button aria-keyshortcuts="h">Help</button> <!-- Will be ignored -->
</main>
<dialog>
…
</dialog>
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 is 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.
If you’re using overrides, pass the same override config both to hotkeyKeyUX()
and getHotKeyHint()
for accurate hints:
import {
getHotKeyHint,
hotkeyOverrides,
hotkeyKeyUX,
startKeyUX
} from 'keyux'
let config = { 'alt+b': 'b' }
startKeyUX(window, [
hotkeyKeyUX([hotkeyOverrides(config)]) // Override B to Alt + B
])
getHotKeyHint(window, 'b', [hotkeyOverrides(config)]) // Alt + B
One-letter hotkeys (like B) will be ignored if user’s focus is inside text inputs or focus groups. This is why for general hotkeys we recommend add some modifier like Alt+B.
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);
}
}
overriding
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 overriding hotkeys using tranforms. Use the hotkeyOverrides()
tranformer with hotkeyKeyUX()
and getHotKeyHint()
.
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', [hotkeyOverrides(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-hotkeys
from 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" tabIndex={-1}>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" tabIndex={-1}>Add to card</button>
</div>
It’s common to use the Meta (or ⌘) modifier for hotkeys on Mac, while Windows and Linux usually favor the Ctrl key. To provide familiar experience on all platforms, enable the Mac compatibility transform:
import {
hotkeyMacCompat,
hotkeyKeyUX,
startKeyUX,
getHotKeyHint
} from 'keyux'
const mac = hotkeyMacCompat();
startKeyUX(window, [hotkeyKeyUX([mac])])
getHotKeyHint(window, 'ctrl+b', [mac]) // Ctrl+B on Windows/Linux and ⌘+b on Mac
Hotkeys pressed with the Meta modifier will work as if the Ctrl modifier was pressed.
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 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. User can search the menu item by typing the first characters of the item text.
To enable this feature, call focusGroupKeyUX
.
import { focusGroupKeyUX } from 'keyux'
startKeyUX(window, [
focusGroupKeyUX()
])
The role="listbox"
is used for lists from which a user may select one or
more items which are static and, unlike HTML <select>
elements,
may contain images.
<ul role="listbox">
<li tabindex="0" role="option">Pizza</li>
<li tabindex="0" role="option">Sushi</li>
<li tabindex="0" role="option">Ramen</li>
</ul>
Users will use Tab to get inside the listbox, and will use either arrows or Home, End or an item name to navigate inside.
To enable this feature, call focusGroupKeyUX
.
import { focusGroupKeyUX } from 'keyux'
startKeyUX(window, [
focusGroupKeyUX()
])
The role="tablist"
identifies the element that serves as the container for a set of tabs.
The tab content should be marked by [role="tabpanel']
.
<div role="tablist">
<button role="tab">Home</button>
<button role="tab">About</button>
<button role="tab">Contact</button>
</div>
Users will use Tab to get inside the tablist, and will use either arrows or Home, End.
To enable this feature, call focusGroupKeyUX
.
import { focusGroupKeyUX } from 'keyux'
startKeyUX(window, [
focusGroupKeyUX()
])
The role="toolbar"
defines the containing element as a collection of commonly used function buttons
or controls represented in a compact visual forms. Buttons inside the toolbar
must have type="button"
attribute because the default one is submit
.
<div role="toolbar">
<div>
<button type="button">Copy</button>
<button type="button">Paste</button>
<button type="button">Cut</button>
</div>
<div>
<input type="checkbox" />
</div>
</div>
Users will use Tab to get inside the tablist, and will use either arrows or Home, End.
To enable this feature, call focusGroupKeyUX
.
import { focusGroupKeyUX } from 'keyux'
startKeyUX(window, [
focusGroupKeyUX()
])
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 { focusGroupKeyUX, jumpKeyUX } from 'keyux'
startKeyUX(window, [
focusGroupKeyUX(),
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 { focusGroupKeyUX, jumpKeyUX, hiddenKeyUX } from 'keyux'
startKeyUX(window, [
focusGroupKeyUX(),
jumpKeyUX(),
hiddenKeyUX()
])