Focus & Navigation
Focus management, keyboard routing, selection, and pointer events
Gridland provides a unified focus and input system that works identically in both terminal and web runtimes.
Focus
Examples
Spatial Navigation
Focus with Multi-Select
Usage
import { FocusProvider, useFocus, useKeyboard, useShortcuts, useFocusedShortcuts, FocusScope } from "@gridland/utils"
import { GridlandProvider, StatusBar } from "@gridland/ui"
// 1. Wrap your app with FocusProvider
function App() {
return (
<GridlandProvider theme={darkTheme} useKeyboard={useKeyboard}>
<FocusProvider selectable>
<box flexDirection="row" gap={1}>
<Cell id="inbox" label="Inbox" autoFocus />
<Cell id="drafts" label="Drafts" />
<Cell id="sent" label="Sent" />
</box>
<AppStatusBar />
</FocusProvider>
</GridlandProvider>
)
}
// 2. Make components focusable with useFocus
function Cell({ id, label, autoFocus }) {
const { isFocused, isSelected, focusId, focusRef } = useFocus({ id, autoFocus })
// 3. Handle keyboard input when focused
useKeyboard((e) => {
if (e.name === "return") console.log("submitted", label)
}, { focusId, selectedOnly: true })
// 4. Register shortcuts for the status bar
useShortcuts(
isSelected
? [{ key: "enter", label: "submit" }, { key: "esc", label: "back" }]
: [{ key: "↑↓←→", label: "navigate" }, { key: "enter", label: "select" }],
focusId,
)
return (
<box ref={focusRef} border borderColor={isSelected ? "blue" : isFocused ? "green" : "gray"}>
<text>{label}</text>
<text>{isSelected ? "selected" : "not selected"}</text>
</box>
)
}
// 5. Display context-sensitive shortcuts
function AppStatusBar() {
const shortcuts = useFocusedShortcuts()
return <StatusBar items={shortcuts} />
}How it works
FocusProvider enables the focus system. Pass selectable to enable two-layer focus: components are first focused (highlighted), then selected (entered for interaction) with Enter. Escape deselects.
useFocus makes a component focusable. It returns:
isFocused/isSelected/isAnySelectedfor styling based on focus statefocusIdto pass to other hooks likeuseKeyboardanduseShortcutsfocusRefto attach to the root element so arrow-key navigation can measure positions
| Option | Type | Default | Description |
|---|---|---|---|
id | string | auto-generated | Unique identifier for this focusable |
tabIndex | number | 0 | Tab order. -1 to skip tab navigation |
autoFocus | boolean | false | Focus immediately on mount |
disabled | boolean | false | Skip in tab order when true |
Navigation works two ways: Tab/Shift+Tab cycles in linear order, arrow keys navigate spatially to the nearest neighbor. If a component handles arrows internally (e.g., a SelectInput), call event.preventDefault() to prevent the FocusProvider from also navigating.
useKeyboard handles keyboard input. Pass focusId to scope it to the focused component, or global: true to always listen.
| Option | Type | Description |
|---|---|---|
focusId | string | Only fire when this ID is focused |
global | boolean | Always fire regardless of focus |
release | boolean | Include key release events |
useShortcuts registers keyboard hints tied to a focusId. They update automatically as the user navigates. useFocusedShortcuts reads the active shortcuts for display in a StatusBar.
FocusScope
Constrain navigation to a group of components. When trap is enabled, Tab wraps around instead of leaving the scope. Useful for modals and menus.
<FocusScope trap autoFocus restoreOnUnmount>
<TextInput label="Name" />
<TextInput label="Email" />
<button onClick={onClose}>Close</button>
</FocusScope>| Prop | Type | Default | Description |
|---|---|---|---|
trap | boolean | false | Prevent Tab from leaving the scope |
autoFocus | boolean | false | Focus first element on mount |
restoreOnUnmount | boolean | true | Restore previous focus on unmount |
Pointer Events
Usage
import { useState } from "react"
const colors = ["#ef4444", "#3b82f6", "#22c55e", "#8b5cf6"]
const names = ["Red", "Blue", "Green", "Purple"]
function PointerDemo() {
const [selected, setSelected] = useState<number | null>(null)
const [mousePos, setMousePos] = useState<{ x: number; y: number } | null>(null)
return (
<box flexDirection="column" padding={1}>
{/* Click to select a color */}
<box flexDirection="row" gap={1}>
{colors.map((color, i) => (
<box
key={color}
flexGrow={1}
height={3}
border
borderColor={i === selected ? color : "#555"}
onMouseDown={(e) => {
setSelected(i)
setMousePos({ x: e.x, y: e.y })
}}
>
<text style={{ fg: color, bold: i === selected }}>{names[i]}</text>
</box>
))}
</box>
{/* Hover detection */}
<HoverBox />
{/* Display click result */}
<text>{selected !== null ? `Clicked ${names[selected]}` : "Click a color"}</text>
<text style={{ dim: true }}>{mousePos ? `mouse: ${mousePos.x}, ${mousePos.y}` : ""}</text>
</box>
)
}
function HoverBox() {
const [hovering, setHovering] = useState(false)
return (
<box
border
borderColor={hovering ? "#22c55e" : "#555"}
onMouseOver={() => setHovering(true)}
onMouseOut={() => setHovering(false)}
>
<text>{hovering ? "Mouse inside!" : "Hover me"}</text>
</box>
)
}How it works
All intrinsic elements (<box>, <text>, etc.) support mouse event props. Each event receives an object with x, y coordinates relative to the element.
onClick is a synthetic event that fires when mouseDown and mouseUp occur on the same element.
| Event | Description |
|---|---|
onMouseDown | Mouse button pressed |
onMouseUp | Mouse button released |
onClick | Press and release on the same element |
onMouseOver | Cursor enters the element |
onMouseOut | Cursor leaves the element |
onMouseScroll | Scroll wheel over the element |
Cursor Highlight
Usage
<TUI cursorHighlight cursorHighlightColor="#8888ff" cursorHighlightOpacity={0.2}>
<box border>
<text>Move the mouse over cells to see the highlight</text>
</box>
</TUI>How it works
Cursor highlight adds a visual overlay on the cell under the mouse cursor. Enable it on the root TUI component.
| Prop | Type | Default | Description |
|---|---|---|---|
cursorHighlight | boolean | false | Enable cell highlighting |
cursorHighlightColor | string | gray | CSS color for highlight |
cursorHighlightOpacity | number | 0.15 | Opacity of highlight |