Focus and input routing

How keyboard events flow through your UI and how to manage focus predictably.

What is focus?

Focus determines which element receives keyboard events. In practice, you want:

- global shortcuts handled at the renderer level
- editing/navigation handled by the focused widget (input, list, editor)
- a predictable way to move focus between widgets (Tab / Shift+Tab)

Focus APIs

ConceptWhereNotes
focus()Renderable instanceRoutes keyboard input to this node (common for inputs/selects/textareas).
focusedReact/Solid propConvenient way to request focus on mount (use sparingly; prefer explicit focus manager for complex apps).
idAll nodesUse stable ids to implement predictable tab order and focus restore.
renderer.root.getRenderable(id)Renderer rootLookup helper for imperative focus routing.

A simple focus manager

For multi-panel UIs, a small focus manager is often enough. Keep an ordered list of focusable ids and wrap around when tabbing.

const focusOrder = ["search", "results", "command"]
let focusIndex = 0

function focusById(id: string) {
  const node = renderer.root.getRenderable(id)
  node?.focus?.()
}

renderer.on("key", (event) => {
  if (event.name !== "tab") return
  focusIndex = (focusIndex + 1) % focusOrder.length
  focusById(focusOrder[focusIndex]!)
})

If your UI mounts/unmounts widgets, update the focus order at the same time, otherwise focus will “jump” unexpectedly.

Recipe: modal focus trap

When you open a modal, focus should stay inside it. A simple pattern is to keep a boolean isModalOpen and override Tab routing while it is open.

let isModalOpen = false

renderer.on("key", (event) => {
  if (event.name === "escape" && isModalOpen) {
    isModalOpen = false
    focusById("command")
    return
  }

  if (event.name === "tab" && isModalOpen) {
    // tab inside modal only
    focusById("modal-input")
  }
})