Accepted 2026-10-31 by the owner: “Accept both” (asked directly in design-system session c6,
STA-1371). Proposed Built on the owner’s direction of the same day: hints use HeroUI’s free Kbd
(“already heorui community has … kbd”); people can remap keys, there is a shortcuts reference, and
a toggle makes the keys “visible or hidden by default — if hidden we can have tooltip and if
visible kbd”; single-key shortcuts can be turned off (“Yes, user can turn off”); and it builds on
the existing
@stackra/shortcuts (“we have our own stackra kbd or commands or shortcuts
package”).Context
@stackra/shortcuts declares app-level shortcuts as classes (Shortcut({ keys, group }), ADR 0294 decision 23):
parsing with chords, a conflict detector, key hints and keyboard bindings through TanStack Hotkeys. Nothing used it
from the design system. @stackra/ui drew key hints as ready-made text (MenuLeafItem.shortcut = "⌘E", a sidebar
item’s shortcut, a hard-coded esc in the drawer) and bound no key; the command palette had its own window
keydown listener for Cmd+K. So a hint could lie, a key could not be remapped, and a page shortcut kept firing under an
open dialog.
Decision
-
One action definition. A UI action is
ShortcutAction { id, label, icon, shortcut, run, when, permission }(plusgroup,scope,isDisabled,isDirectional), a client-side descriptor in@stackra/shortcuts. The same object feeds the button (<Button action={save} />), the menu item, the palette entry, the tooltip and the cheat sheet. It is not the serverActionkind of@stackra/actions:runmay call one,permissionnames the permission the host checks, andidis what remaps key on. A component also takes the loose formshortcut="mod+s"(with an optionalshortcutId) next to its ownonPress. -
Dependency direction.
@stackra/uidepends on@stackra/shortcuts(workspace:*), one way;@stackra/shortcutsnever imports@stackra/ui. Parsing, display (formatShortcutKeys), matching, conflict detection, the settings layers and the run-time registry (ShortcutProvider,useShortcutAction,useShortcutRegistry,ShortcutOverlayScope) live in@stackra/shortcuts(.for the pure parts,./reactfor the provider and hooks);@stackra/uiadds only the prop contract (ShortcutContractProps), the hint part on HeroUI’s Kbd (ShortcutHint), the component wiring (useActionShortcut), the cheat sheet and the settings panel. Both aretier:lib, so turbo boundaries hold; no cycle exists for knip or the build. The new provider needs no DI container, so it sits besideShortcutBindings(which binds theShortcutclasses) and both may run. Every change to@stackra/shortcutsis additive. -
Registration and scopes.
DesignSystemProvidermountsShortcutProvider(one capture-phasewindowkey listener) and the cheat sheet. A component registers while mounted. Four scopes, narrowest first: focus (only while focus is inside an element: a table, a carousel), overlay (only the topmost open modal, drawer, panel or popover, throughShortcutOverlayScope), page (paused while any overlay is open) and app (always: the palette Mod+K, the cheat sheet?, Ask Tamo Mod+J). When two match, the narrower scope wins, then the latest registered. -
Hint modes.
visibledraws an inline HeroUIKbdbeside the action;hiddenputs the keys only in the action’s tooltip (Tooltip shortcut). A control with no room for an inline hint (an icon button) always uses the tooltip. Holding Mod (Command or Control) alone for 500 ms reveals a Kbd badge over every visible action that shows no inline hint, in either mode; releasing Mod or pressing another key hides them. Hints are HeroUI’s freeKbdonly; there is no custom key chip and no HeroUI Pro. -
Three settings layers, all runtime configuration.
ShortcutSettings { enabled, hintMode, singleKey, remaps, locked }, folded byresolveShortcutSettings(platform, tenant, person):The UI never stores them:ShortcutSettingsPaneltakesvalueandonChange, andDesignSystemProvidertakesshortcutsandonShortcutsChange. A tenant changes a default without a release. -
Platform labels and layouts. Bindings are written with
mod(Command on Apple devices, Control elsewhere) and drawn per platform (⌘K,Ctrl+K). Matching readsevent.code, the physical key, somod+kfires on an Arabic layout where that key typesن; labels stay Latin and isolated left to right in Arabic. A binding markedisDirectional(next and previous) swaps Left and Right on a right-to-left page, for matching and for the hint. -
Typing guard and IME. A step with no command modifier (
e,/, arrows) never fires while focus is in a text field, a select or an editable element; Escape, the function keys and modified keys still do (Mod+Enter submits from a text area). A key pressed during IME composition (isComposing, key code 229) is ignored. WithsingleKeyoff, every binding made of one character key (Shift allowed) is skipped (WCAG 2.2 SC 2.1.4). -
Reserved keys.
RESERVED_SHORTCUTSholds what the browser or the system keeps (Mod+W, Mod+T, Mod+N, Mod+R, Mod+L, Mod+Q, zoom, Ctrl+Tab, Alt+F4, F5, F11, F12, …).findShortcutConflicts(…, { reserved: true })reports them asreserved; the key recorder refuses to save one and warns on a clash with another action or a standard binding. Mod+J is reserved inSTANDARD_SHORTCUTSfor “Ask Tamo” (ADR 0507), so no app binding takes it silently. -
Sequences, availability. Two-key sequences (
g o) waitCHORD_TIMEOUT(1.5 s) for the second key. A disabled action, or one whosepermissionthe host’scanrefuses, is not registered: its key does nothing and it is not listed;whenis checked at key time and, when false, lets the key fall through. -
Accessibility. Every bound control carries
aria-keyshortcuts(Meta+K,Control+Shift+L; none for a chord, which the attribute cannot express). An inline hint inside a control isaria-hidden; a hint on its own (the cheat sheet) is read as its spoken form (“Command K”, “G, then O”). -
Touch and native. On web, hints hide on a touch screen until a hardware key is pressed or a fine pointer is
present. On native the contract props are accepted and draw nothing:
@stackra/ui/nativehas no hardware-keyboard handling today, so iPad and Android hardware-keyboard shortcuts and hints are a later step (an Expo module per ADR 0478 behind a port). -
The cheat sheet and the palette read the registry.
?(Shift+/) opens a dialog generated from what is registered right now, grouped by the action’s group or its scope, searchable (case, accents and Arabic diacritics folded), in every locale.CommandPaletteregisters its own key (remappable) instead of awindowlistener and lists every registered action with its keys in an “Actions” section.
Consequences
- A product gets keyboard support by passing
shortcutoraction; the hint, the tooltip, the cheat sheet and the palette can no longer disagree with the key. MenuLeafItem.shortcutandCommandItem.shortcutnow mean a binding; an older ready-made string (⌘E) is still drawn as given and binds nothing.- The palette’s Cmd+K needs a
ShortcutProvider(mounted byDesignSystemProvider); a palette rendered outside one opens only throughisOpen. - Native shows no hints until the hardware-keyboard step lands.
Enforcement
@stackra/shortcutstests cover matching (shortcut-aware-ui.function.test.ts), the registry’s scopes, typing guard, IME, single-key, sequences, permissions and reveal (create-shortcut-registry.function.test.ts) and the provider and hooks (use-shortcut-action.test.tsx); the entry tests pin the new exports.@stackra/uicomponent tests press the keys through aShortcutProvider.- planned — a lint rule refusing a
window.addEventListener("keydown")in@stackra/uioutside@stackra/shortcuts. - planned — a gen check that a tenant or person remap names a registered action id.
Supersedes and sources
Supersedes: none. Sources: the owner’s answers of 2026-10-31 in the c6 session; HeroUI’s Kbd documentation (heroui.com/en/docs/react/components/kbd); WAI-ARIA 1.2 aria-keyshortcuts; WCAG 2.2 SC 2.1.4.