Input & Focus¶
Rezi routes input deterministically through a focus system that manages keyboard navigation and event delivery.
Identity: id vs key¶
Two identity systems serve different purposes:
| Prop | Purpose | Example |
|---|---|---|
id |
Focus management and event routing | ui.button({ id: "save", label: "Save" }) |
key |
Reconciliation stability in lists | ui.text(item.name, { key: item.id }) |
These must not be conflated:
idmust be unique across the committed widget treekeyonly needs to be unique among siblings- Non-interactive widgets may omit
id - Dynamic lists should always provide
key
Focus Navigation¶
Focusable widgets (buttons, inputs, selects) participate in Tab navigation:
- Tab - Move focus forward
- Shift+Tab - Move focus backward
- Enter/Space - Activate focused widget
- Arrow keys - Navigate within widgets (lists, tables)
Focus Order¶
Focus order follows document order (depth-first tree traversal):
ui.column({}, [
ui.button({ id: "first", label: "1" }), // Tab stop 1
ui.row({}, [
ui.button({ id: "second", label: "2" }), // Tab stop 2
ui.button({ id: "third", label: "3" }), // Tab stop 3
]),
ui.button({ id: "fourth", label: "4" }), // Tab stop 4
])
Focus Zones¶
Group related widgets with focus zones for organized Tab navigation:
ui.column({}, [
ui.focusZone({ id: "toolbar" }, [
ui.button({ id: "new", label: "New" }),
ui.button({ id: "open", label: "Open" }),
]),
ui.focusZone({ id: "form" }, [
ui.input({ id: "name", value: state.name }),
ui.button({ id: "submit", label: "Submit" }),
]),
])
Tab moves between zones; arrow keys navigate within zones.
Focus Traps¶
Constrain focus within modals and overlays:
ui.focusTrap({ id: "modal", active: state.showModal }, [
ui.text("Confirm action?"),
ui.button({ id: "ok", label: "OK" }),
ui.button({ id: "cancel", label: "Cancel" }),
])
Keybindings¶
Basic Keybindings¶
Register global keyboard shortcuts with app.keys():
app.keys({
"ctrl+s": () => save(),
"ctrl+q": () => app.stop(),
"escape": () => closeModal(),
"f1": () => showHelp(),
});
Modifier Keys¶
Supported modifiers: ctrl, alt, shift, meta
app.keys({
"ctrl+s": () => save(),
"ctrl+shift+s": () => saveAs(),
"alt+f": () => openFileMenu(),
"meta+q": () => quit(), // Cmd on macOS
});
Chord Sequences¶
Chords are key sequences pressed in succession (like Vim's gg or Emacs's C-x C-s):
app.keys({
"g g": () => scrollToTop(), // Press g twice
"g e": () => scrollToEnd(), // Press g then e
"ctrl+x ctrl+s": () => save(), // Emacs-style
"d d": () => deleteLine(), // Vim-style
});
Chord timeout is 1000ms by default.
Key Context¶
Key handlers receive a context object with state access:
app.keys({
"j": (ctx) => ctx.update((s) => ({ ...s, cursor: s.cursor + 1 })),
"k": (ctx) => ctx.update((s) => ({ ...s, cursor: s.cursor - 1 })),
});
Modal Keybinding Modes¶
For Vim-style modal editing, use app.modes():
app.modes({
normal: {
"i": () => app.setMode("insert"),
"v": () => app.setMode("visual"),
"j": (ctx) => ctx.update(moveCursorDown),
"k": (ctx) => ctx.update(moveCursorUp),
"d d": (ctx) => ctx.update(deleteLine),
":": () => openCommandLine(),
},
insert: {
"escape": () => app.setMode("normal"),
},
visual: {
"escape": () => app.setMode("normal"),
"y": (ctx) => { yank(); app.setMode("normal"); },
"d": (ctx) => { deleteSelection(); app.setMode("normal"); },
},
});
// Start in normal mode
app.setMode("normal");
Query current mode with app.getMode().
Event Handling¶
Widget Events¶
Interactive widgets receive events through callback props:
ui.button({
id: "submit",
label: "Submit",
onPress: () => handleSubmit(),
})
ui.input({
id: "name",
value: state.name,
onInput: (value) => app.update((s) => ({ ...s, name: value })),
onBlur: () => validateName(),
})
ui.select({
id: "country",
value: state.country,
options: countries,
onChange: (value) => app.update((s) => ({ ...s, country: value })),
})
Global Event Handler¶
For centralized event handling, use app.onEvent():
const unsubscribe = app.onEvent((ev) => {
if (ev.kind === "action") {
console.log(`Action: ${ev.id} / ${ev.action}`);
}
});
// Later: unsubscribe();
Event Types¶
Rezi exposes two event layers:
Engine Events - Low-level events decoded from the ZREV protocol:
- Key events with modifiers and key codes
- Mouse events with position and button state
- Resize events
- Tick events for animations
Routed UI Events - High-level actions:
{ kind: "action", id: "btn", action: "press" }- Button activation{ kind: "action", id: "input", action: "input", value, cursor }- Text input{ kind: "action", id: "select", action: "change", value }- Selection change
Determinism¶
Rezi's focus and event routing is deterministic:
- Same widget tree produces the same focus order
- Same input sequence produces the same routed events
- No timing-dependent behavior in the core
This enables:
- Reproducible testing with event sequences
- Predictable user experience
- Debuggable event flows
Next Steps¶
- Styling - Colors, themes, and visual customization
- Focus Zone - Focus zone widget reference
- Focus Trap - Focus trap widget reference