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
| Concept | Where | Notes |
|---|---|---|
focus() | Renderable instance | Routes keyboard input to this node (common for inputs/selects/textareas). |
focused | React/Solid prop | Convenient way to request focus on mount (use sparingly; prefer explicit focus manager for complex apps). |
id | All nodes | Use stable ids to implement predictable tab order and focus restore. |
renderer.root.getRenderable(id) | Renderer root | Lookup 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")
}
})