On this page
@inklu/keys
Updated October 2026
@inklu/keys is a command system for the web. A command is something your app can do: an id, a title, a handler. A binding is a key that triggers it. Keeping them apart lets one command run from the keyboard, a palette, a menu or code, and lets users rebind it without touching the definition. Key matching comes from TanStack Hotkeys.
Installation
pnpm add @inklu/keys
Three entry points: @inklu/keys (framework-agnostic), @inklu/keys/react (hooks; needs React) and @inklu/keys/attributes (plain markup).
Quick start
Define every command in one place. Ids come from the object keys, so commands.run("doc.save") autocompletes and a typo is a type error.
Mod is ⌘ on macOS and Ctrl elsewhere.
AI setup
Paste this prompt into Claude Code, Cursor or any coding agent with write access to your project. It needs no other context.
For an agent that keeps working in the repo, install the agent skill instead. It carries the rules, not just the setup:
pnpm dlx skills add nkurunziza-saddy/inklu --skill inklu-keys
Agents reading the site start at /keys/llms.txt. The complete reference, including what this page leaves out, is /keys/llms-full.txt.
React
Call useMountCommands once, near the root. There is no provider: every hook takes the registry as its first argument, which keeps command ids typed.
Command palette
The registry already lists every command with its current shortcut. Add group, keywords and hidden to definitions, and filter with matchCommands:
Bindings
An array of keys means alternatives. Sequences use seq and must be pressed within sequenceTimeout (1000 ms by default).
Scopes
Push a scope while a dialog is open instead of writing if (!modalOpen) in every handler. An exclusive scope silences everything beneath it; a command scoped "*" always fires.
To take back a single key, skip exclusive: a scoped command already outranks a global one on the same key.
Rebinding
Bindings live in a keymap of defaults plus user overrides, so reset is exact. Persist overrides by giving the registry a storage:
useRebind records keys, reports conflicts and writes the result. Other commands are silenced while it records.
Behaviour
- One keypress runs one command. A scoped command beats a global one, then the later registration wins. A winner whose
whenfails steps aside. - Inputs. In text fields only
Ctrl/Metachords and Escape fire. Override withignoreInputs. - Events.
preventDefault()is called only when a command runs. Propagation is never stopped. - Debugging.
commands.whyNotRun(id)returns"disabled","out-of-scope","guarded","unknown"ornull. - Server rendering. Shortcuts render as Ctrl until
mount(), then switch to the real platform. Passplatformto pin it.
Accessibility
Put each command's aria string on the control that runs it. It holds key names (Meta+K), which is what screen readers expect, not glyphs.
Sequences have no aria-keyshortcuts form, so aria is empty for them; use spoken ("G then I") instead. Offer a setting wired to setSingleKeyShortcuts(false) so users can turn off single-key shortcuts (WCAG 2.1.4).
API
React hooks: useMountCommands, useCommands, useCommand, useShortcut, useScope, useRegisterCommand, useRebind, usePendingSequences, useHeldKeys.
Runtime commands, package-shipped bindings, the data-command markup API, conflict kinds and createRebinder are in the README, also served for agents as /keys/llms-full.txt.