Skip to main content
Rezi provides comprehensive input handling with keybindings, focus management, and event routing.

Keyboard Input

Basic Keybindings

Register global keybindings with app.keys():
import { createNodeApp } from "@rezi-ui/node";
import { ui } from "@rezi-ui/core";

const app = createNodeApp({ initialState: { count: 0 } });

// Single key
app.keys("q", () => {
  app.exit();
});

// Key with modifiers
app.keys("ctrl+c", () => {
  app.exit();
});

// Multiple bindings
app.keys({
  "up": () => app.update(s => ({ count: s.count + 1 })),
  "down": () => app.update(s => ({ count: s.count - 1 })),
  "space": () => app.update(s => ({ count: 0 })),
});

Key Syntax

app.keys("ctrl+s", handleSave);      // Ctrl+S
app.keys("alt+enter", handleSubmit); // Alt+Enter
app.keys("meta+k", openCommand);     // Cmd+K (macOS) / Win+K (Windows)
app.keys("ctrl+shift+p", openPalette);
Modifiers: ctrl, alt, shift, meta

Key Chords

Multi-key sequences with timeout:
app.keys("g g", goToTop);      // Press 'g' twice
app.keys("ctrl+k ctrl+c", toggleComment);
Chord timeout defaults to 1000ms. If the second key isn’t pressed within the timeout, the chord is cancelled.

Widget Events

Handle widget-specific events with app.on():
app.on("event", (event, state) => {
  if (event.action === "press" && event.id === "save") {
    // Button was pressed
    return { ...state, saved: true };
  }
  
  if (event.action === "input" && event.id === "username") {
    // Text input changed
    return { ...state, username: event.value };
  }
  
  if (event.action === "select" && event.id === "theme") {
    // Dropdown selection changed
    return { ...state, selectedTheme: event.value };
  }
});

Event Actions

Emitted by buttons when activated:
if (event.action === "press" && event.id === "submit") {
  // Handle button press
}

Focus Management

Tab Navigation

Focusable widgets automatically participate in tab navigation:
ui.column({ gap: 1 }, [
  ui.input({ id: "username", value: state.username }),  // Tab order: 1
  ui.input({ id: "password", value: state.password }),  // Tab order: 2
  ui.button({ id: "login", label: "Login" }),          // Tab order: 3
])
Press Tab to move forward, Shift+Tab to move backward.

Focus Zones

Create isolated focus regions:
ui.focusZone({ id: "sidebar" }, [
  ui.button({ id: "home", label: "Home" }),
  ui.button({ id: "settings", label: "Settings" }),
  ui.button({ id: "help", label: "Help" }),
])
Focus zones restrict tab navigation to their children. Use arrow keys within zones for fine-grained navigation.

Focus Traps

Trap focus within modals and dialogs:
ui.focusTrap({ id: "modal" }, [
  ui.modal({
    id: "confirm",
    title: "Confirm Action",
    open: state.showModal,
  }, [
    ui.text("Are you sure?"),
    ui.actions([
      ui.button({ id: "cancel", label: "Cancel" }),
      ui.button({ id: "confirm", label: "Confirm", intent: "primary" }),
    ]),
  ]),
])
Focus cannot escape a focus trap with Tab. Use Escape to close the modal and restore previous focus.

Focus Change Events

Track focus changes:
app.onFocusChange((focusInfo) => {
  console.log(`Focus moved to: ${focusInfo.focusedId}`);
  console.log(`Previous focus: ${focusInfo.previousId}`);
});

Interactive Widget IDs

All interactive widgets (button, input, select, checkbox, etc.) require a unique id prop. Omitting id causes a runtime crash.
// ✅ Correct
ui.button({ id: "save", label: "Save" })
ui.input({ id: "username", value: state.username })

// ❌ Wrong - missing id
ui.button({ label: "Save" })  // Runtime error!

Dynamic IDs in Lists

Use ctx.id() inside defineWidget for unique IDs:
import { defineWidget, each } from "@rezi-ui/core";

const TodoItem = defineWidget<{ id: string; text: string }>((props, ctx) => {
  return ui.row({ gap: 1 }, [
    ui.checkbox({ id: ctx.id("check") }),  // Unique per instance
    ui.text(props.text),
  ]);
});

const view = (state) => ui.column({ gap: 1 }, [
  each(state.todos, (todo) => TodoItem({ id: todo.id, text: todo.text })),
]);

Keybinding Modes

Create modal keybinding contexts:
import { createNodeApp } from "@rezi-ui/node";

const app = createNodeApp({ initialState: { mode: "normal" } });

// Normal mode
app.keys("i", () => {
  app.setMode("insert");
  return { mode: "insert" };
});

// Insert mode (different bindings)
app.keys("escape", () => {
  app.setMode("normal");
  return { mode: "normal" };
}, { mode: "insert" });

Raw Key Events

Access raw key events for custom handling:
app.on("key", (keyEvent, state) => {
  console.log(`Key: ${keyEvent.key}`);
  console.log(`Modifiers: ctrl=${keyEvent.ctrl}, alt=${keyEvent.alt}`);
  
  // Return undefined to let default handlers run
  // Return state update to handle the key
});

Focus Styling

Customize focus indicators via theme:
import { createThemeDefinition } from "@rezi-ui/core";

const myTheme = createThemeDefinition("custom", {
  // ... color tokens ...
}, {
  focus: {
    indicator: "ring",        // "ring" | "bracket" | "arrow" | "dot" | "caret"
    ringVariant: "double",    // For ring indicator
    bracketSet: "square",     // For bracket indicator
    arrowSet: "standard",     // For arrow indicator
  },
});

Best Practices

Unique IDs

Always provide unique id props for interactive widgets. Use ctx.id() in lists to prevent collisions.

Standard Keys

Follow platform conventions: Ctrl+C/Ctrl+V on Windows/Linux, Cmd+C/Cmd+V on macOS. Use meta modifier for cross-platform shortcuts.

Focus Traps

Always use focus traps for modals and dialogs. Users expect Tab to cycle within the modal, not escape to background content.

Escape Key

Reserve Escape for “back” or “cancel” actions. Users expect Escape to close overlays and return to the previous state.

Next Steps

Mouse Support

Handle mouse clicks, scrolling, and drag events

Animation

Add smooth transitions and animations

Build docs developers (and LLMs) love