useKeyboard
Subscribe to keyboard events
The useKeyboard hook subscribes to keyboard events inside a <TUI> component.
By default it receives press events (including key repeats). Use options.release
to also receive release events.
For focus-aware components, prefer useInteractive({ id }).onKey(handler)
over useKeyboard({ focusId }). useInteractive composes focus registration,
selection-scoped keyboard routing, and shortcut hints in a single call see
Focus for the full model. Reach for useKeyboard
directly when you need a global shortcut, a raw listener outside the focus
system, or a custom reducer on top of KeyEvent.
import { useKeyboard } from "@gridland/utils"Usage
Pass { global: true } for app-level handlers and { focusId } to scope a handler to a
focus id. Calling useKeyboard(handler) without an options bag is deprecated see below.
useKeyboard((event) => {
if (event.name === "q") quit()
if (event.name === "return") submit()
}, { global: true })useKeyboard((event) => {
if (event.ctrl && event.name === "s") save()
if (event.shift && event.name === "Tab") focusPrevious()
}, { global: true })useKeyboard((event) => {
if (event.eventType === "release") {
heldKeys.delete(event.name)
} else {
heldKeys.add(event.name)
}
}, { global: true, release: true })Focus-aware routing (escape hatch)
Most focus-aware components should use
useInteractive({ id }).onKey(handler)
instead of the options below. The raw useKeyboard options are useful when
you need a keyboard listener outside the useInteractive lifecycle: global
shortcuts, status-bar reducers, or components that manage their own focus id
for a reason that useInteractive doesn't cover.
// Low-level form prefer useInteractive({ id }).onKey(handler) for component code
const { focusId } = useInteractive({ id: "my-panel" })
useKeyboard((e) => {
if (e.name === "return") submit()
}, { focusId })useKeyboard((e) => {
if (e.ctrl && e.name === "q") quit()
}, { global: true })Parameters
| Param | Type | Default | Description |
|---|---|---|---|
handler | (key: KeyEvent) => void | -- | Called on each keypress |
options.release | boolean | false | Also receive key release events |
options.focusId | string | -- | Only fire when this ID is focused (from useInteractive()) |
options.global | boolean | -- | Always fire regardless of focus state |
options.selectedOnly | boolean | false | Only fire when the component is selected (entered), not just focused. Requires focusId |
KeyEvent
The callback receives a KeyEvent with every field populated for most keys. Fields marked
Kitty-only are undefined unless the terminal supports the Kitty keyboard protocol.
| Property | Type | Description |
|---|---|---|
name | string | Logical key name ("a", "left", "f1", "escape", "return"). Also set to the literal character for printable punctuation like "[", "]", ";", "/". |
ctrl | boolean | Whether Ctrl was held. |
meta | boolean | Whether Meta was held. Raw parser also sets this for Alt/Option escape sequences meta and option often co-occur. |
shift | boolean | Whether Shift was held. For letters, name is lowercased and shift carries the case. |
option | boolean | Whether Option (macOS) / Alt was held. |
sequence | string | Raw bytes from the terminal. Equals name for single-char input; for named keys this is the ANSI escape sequence ("\x1b[A"), not the name. Prefer name for logic. |
number | boolean | True if the key is a digit 0–9. |
raw | string | Literal pre-parse bytes. Rarely needed prefer sequence or name. |
eventType | "press" | "release" | "repeat" | Usually "press". Releases and repeats require Kitty keyboard protocol plus { release: true }. |
source | "raw" | "kitty" | Which parser produced the event. |
code | string | undefined | ANSI code ("[A") or Kitty CSI-u code ("[57352u") for escape sequences. Undefined for single-char input. |
super | boolean | undefined | Super / Windows / Command key. Populated by Kitty and by modifyOtherKeys in raw mode. |
hyper | boolean | undefined | Hyper key. Populated by Kitty and by modifyOtherKeys in raw mode. |
capsLock | boolean | undefined | Kitty-only. Whether Caps Lock was on. |
numLock | boolean | undefined | Kitty-only. Whether Num Lock was on. |
baseCode | number | undefined | Kitty-only. Base-layout codepoint (for QWERTY vs Dvorak disambiguation). |
repeated | boolean | undefined | Kitty-only. True for key-repeat events. |
KeyEvent also exposes defaultPrevented / propagationStopped (read-only getters) and
preventDefault() / stopPropagation() methods. preventDefault() suppresses the framework's
focus-navigation logic and stops renderable-scoped listeners from firing. stopPropagation()
additionally halts any remaining global listeners. Both are honored by the key dispatcher
(KeyHandler) at runtime.
Which field should I read?
Prefer name for almost everything. Reach for sequence or raw only when detecting a
specific byte pattern that name does not normalize.
import { useKeyboard, type KeyEvent } from "@gridland/utils"
useKeyboard((event: KeyEvent) => {
// Letters with modifiers → name + modifier flag
if (event.ctrl && event.name === "s") save()
// Arrow keys and named keys → name
if (event.name === "left") prevItem()
if (event.name === "escape") closeModal()
// Function keys → name
if (event.name === "f1") showHelp()
// Printable punctuation → name (it is set to the character)
if (event.name === "[") prevPage()
if (event.name === "]") nextPage()
}, { global: true })Deprecation: bare form
Calling useKeyboard(handler) without an options bag is deprecated. Pass { global: true }
explicitly for app-level shortcuts, or { focusId } for focus-scoped handlers. The bare form
still works at runtime this deprecation exists so every call site states its intent at
the source. @gridland/utils ships an overloaded @deprecated type signature so IDE hover
surfaces the warning on bare calls.
useKeyboard((event) => { /* ... */ })useKeyboard((event) => { /* ... */ }, { global: true })