React keymap integration
@opentui/keymap/react puts a pre-created OpenTUI keymap in React context and connects keymap state to React renders.
These bindings require Keymap<Renderable, KeyEvent> from @opentui/keymap/opentui.
They do not create or wrap the HTML adapter for browser React apps.
Read React bindings for OpenTUI React setup. Read Keymap hosts for keymap construction.
Exports
The entry point exports these runtime values:
| Export | Purpose |
|---|---|
KeymapProvider | Put an existing OpenTUI keymap in React context. |
useKeymap() | Read that keymap. |
useActiveKeys(options?) | Read active press keys and update on keymap state changes. |
usePendingSequence() | Read the pending sequence and update on keymap state changes. |
useBindings(createLayer, deps?) | Register a layer for a component lifecycle. |
reactiveMatcherFromStore(subscribe, getSnapshot, predicate?) | Adapt an external store to ReactiveMatcher. |
It also exports these types:
| Type | Purpose |
|---|---|
KeymapProviderProps | Provider props with keymap and optional children. |
UseBindingsTargetRef<TRenderable> | Ref shape with current: TRenderable | null. |
UseBindingsLayer<TRenderable> | Layer shape with React targetRef support. |
Basic setup
/** @jsxImportSource @opentui/react */
import { createCliRenderer } from "@opentui/core"
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
import { KeymapProvider, useBindings } from "@opentui/keymap/react"
import { createRoot } from "@opentui/react"
const renderer = await createCliRenderer()
const keymap = createDefaultOpenTuiKeymap(renderer)
function App() {
useBindings(() => ({
commands: [
{
name: "app.quit",
run() {
renderer.destroy()
},
},
],
bindings: [{ key: "q", cmd: "app.quit" }],
}))
return <text>Press q to quit</text>
}
createRoot(renderer).render(
<KeymapProvider keymap={keymap}>
<App />
</KeymapProvider>,
)The default OpenTUI factory installs the parser and standard field addons. The provider does not add features and does not own renderer cleanup.
Provider behavior
| Prop | Type | Required |
|---|---|---|
keymap | Keymap<Renderable, KeyEvent> | yes |
children | ReactNode | no |
useKeymap() returns the exact provider value. It throws this error outside a provider:
Keymap not found. Wrap the tree in <KeymapProvider>.Keep the provider's keymap instance stable for its mounted lifetime.
renderer.destroy() also unmounts the React root and runs hook cleanup.
See Lifecycle and cleanup.
useBindings()
useBindings(createLayer, deps?) calls useMemo(createLayer, deps) and registers the resulting layer after commit.
The default dependency list is [].
Include every prop or state value that changes the returned layer. If reevaluation returns a different layer object, the hook replaces the registered layer with it. An unrelated render keeps the existing layer.
The hook replaces its layer in one update, so state listeners see one change. The layer keeps the position of its
hook: among layers of the same priority, precedence follows mount order, also while the layer is empty or waits for
a target. Earlier versions moved a re-registered layer to the top.
If the new layer is plain data (plain objects, arrays, primitives, and functions without own properties; no class
instances, Maps, getters, symbol keys, or frozen or sealed objects) and differs only in its functions, the hook
keeps the registered layer: nothing the keymap handed out changes, and commands, binding commands and enabled
checks call the new functions. A store proxy that reports getters is not plain data; spread it. Object values of
command fields, and any attr or require value from a field compiler (a function too), are handed back or matched
by identity, so they must be the same as before; a new one, even an equal one, registers the layer anew. Function
command fields are forwarded like other functions. state is emitted only if an enabled result differs from the
old functions' result or from one read since the last state event, and a pending key sequence through a layer that
turned off is cleared. Commands of a kept layer are keymap-owned objects. Any other layer, and any layer with a
reactive matcher, is replaced as a new registration in the same position.
The layer can include Core fields such as priority, bindings, and commands.
It can also include fields from installed addons, such as enabled.
React replaces the Core target field with targetRef:
| Shape | Required fields | Behavior |
|---|---|---|
| Global | No targetRef or targetMode | Register a global layer. |
| Local descendants | targetRef | Default to targetMode: "focus-within". |
| Local exact focus | targetRef, targetMode: "focus" | Match only the exact focused renderable. |
If targetRef.current is null, the hook waits. It checks the ref after each render and registers when the target
appears. It also replaces the layer if the same ref points to another renderable.
Passing targetMode without targetRef throws:
useBindings local bindings need a targetRefThe hook disposes its layer on component unmount. Layer disposal also unsubscribes reactive matchers.
Reactive reads
Both read hooks subscribe to the batched state event. They remove that subscription on unmount.
const activeKeys = useActiveKeys({ includeMetadata: true })
const pendingSequence = usePendingSequence()useActiveKeys() calls getActiveKeys(options) after each state update.
usePendingSequence() calls getPendingSequence() after each state update.
Use these values for key hints, command lists, leader prompts, and status text. The Core keymap API defines both result shapes and state timing.
Store matchers
reactiveMatcherFromStore() accepts a subscribe function and a snapshot reader.
Without a predicate, it converts the snapshot to boolean. A predicate can derive the boolean value.
const matcher = reactiveMatcherFromStore(store.subscribe, store.getSnapshot, (mode) => mode === "normal")
useBindings(
() => ({
enabled: matcher,
bindings: [{ key: "x", cmd: "editor.delete-line" }],
}),
[matcher],
)The enabled-field addon subscribes when the layer registers. Disposing the layer calls the store's unsubscribe function.
Test the integration
Use createTestRenderer() for framework rendering and input. Create the OpenTUI keymap from its renderer, then render
the provider and drive mockInput.
Use @opentui/keymap/testing for host-independent addon tests.
See Testing for renderer cleanup and input helpers.
Complete example: React keymap