React Bindings
Provider, hooks, and context for using duck-vim in React applications.
KeyProvider, then use hooks like useKeyBind and useKeySequence to register shortcuts declaratively.import {
KeyProvider,
KeyContext,
useKeyCommands,
useKeyBind,
useKeySequence,
useKeyRecorder,
} from '@gentleduck/vim/react'
'use client' directive in Next.js App Router or similar RSC environments.Types
KeyContextValue
The shape of the value provided by KeyContext.
interface KeyContextValue {
registry: Registry
handler: KeyHandler
sequenceManager: SequenceManager
timeoutMs: number
defaultOptions?: Partial<KeyBindOptions>
}
KeyBindHookOptions
Options for useKeyBind. Extends KeyBindOptions with a targetRef.
interface KeyBindHookOptions extends Partial<KeyBindOptions> {
targetRef?: React.RefObject<HTMLElement | null>
}
SequenceHookOptions
Options for useKeySequence. Extends SequenceOptions with a targetRef.
interface SequenceHookOptions extends SequenceOptions {
targetRef?: React.RefObject<HTMLElement | null>
}
KeyRecorderReturn
interface KeyRecorderReturn {
state: KeyRecorderState
start: (target?: HTMLElement) => void
stop: () => void
reset: () => void
}
KeyProvider
Wraps your app (or a subtree) with the keyboard command system. Creates a Registry, KeyHandler, and SequenceManager, attaches the handler on mount, and cleans up on unmount.
interface KeyProviderProps {
debug?: boolean // Enable debug logging
timeoutMs?: number // Sequence timeout (default: 600)
defaultOptions?: Partial<KeyBindOptions> // Default options for all bindings
children: React.ReactNode
}
Example:
<KeyProvider debug={process.env.NODE_ENV === 'development'} timeoutMs={600}>
<App />
</KeyProvider>
KeyContext
Direct access to the context value. Use this for advanced scenarios where the hooks do not cover your needs.
const ctx = React.useContext(KeyContext)
// ctx.registry, ctx.handler, ctx.sequenceManager
useKeyBind, useKeySequence, useKeyCommands) are sufficient.useKeyCommands(commands, options?)
Register multiple bindings at once using a record of key sequences to commands.
function useKeyCommands(
commands: Record<string, Command>,
options?: KeyBindOptions,
): void
Bindings are registered on mount and cleaned up on unmount. If the commands object changes, old bindings are unregistered and new ones are registered.
Example:
function App() {
const navigate = useNavigate()
useKeyCommands({
'g+d': {
name: 'Go to Dashboard',
execute: () => navigate('/dashboard'),
},
'g+s': {
name: 'Go to Settings',
execute: () => navigate('/settings'),
},
'ctrl+k': {
name: 'Command Palette',
execute: () => setOpen(true),
},
}, { ignoreInputs: true })
return <div>...</div>
}
commands object should be stable (memoized or defined outside the component) to avoid unnecessary re-registrations on every render.useKeyBind(binding, handler, options?)
Register a single key binding.
function useKeyBind(
binding: string,
handler: () => void,
options?: KeyBindHookOptions,
): void
The handler function is captured in a ref internally, so it always calls the latest version without needing it in the dependency array.
useKeyBind('ctrl+k', () => setOpen(true), { preventDefault: true })
useKeySequence(steps, handler, options?)
Register a multi-key sequence.
function useKeySequence(
steps: string[],
handler: () => void,
options?: SequenceHookOptions,
): void
Example:
// Press g, then d (within timeout)
useKeySequence(['g', 'd'], () => navigate('/dashboard'))
// Press Ctrl+K, then Ctrl+D
useKeySequence(['ctrl+k', 'ctrl+d'], () => deleteAll())
// Scoped to an element
useKeySequence(['j', 'k'], () => scrollUp(), {
targetRef: listRef,
timeout: 400,
})
useKeyRecorder()
React wrapper around KeyRecorder for building shortcut customization UIs.
function useKeyRecorder(): KeyRecorderReturn
Example:
function ShortcutRecorder() {
const { state, start, stop, reset } = useKeyRecorder()
return (
<div>
<button onClick={() => start()}>
{state.isRecording ? 'Press a key combination...' : 'Click to record'}
</button>
{state.recorded && (
<div>
<span>Recorded: {state.recorded}</span>
<button onClick={reset}>Clear</button>
</div>
)}
{state.isRecording && (
<button onClick={stop}>Cancel</button>
)}
</div>
)
}
Hook comparison
| Hook | Use case | Supports sequences |
|---|---|---|
useKeyBind | Single binding | No |
useKeySequence | Multi-step sequence | Yes |
useKeyCommands | Bulk registration | Via Registry (e.g. 'g+d') |
useKeyRecorder | Settings UI | - |
useKeyBind for most cases. Use useKeySequence when you need explicit multi-step sequences with modifier keys at each step. Use useKeyCommands when registering many bindings at once.