Skip to content

Focus Hooks

Tree-based focus system hooks for managing focus state and navigation.

Import

tsx
import { useFocusable, useFocusWithin, useFocusManager } from "inkx"

useFocusable

Returns focus state for the nearest focusable ancestor. The component must be rendered inside a <Box focusable> with a testID for identification.

Usage

tsx
function FocusableItem({ label }: { label: string }) {
  const { focused } = useFocusable()

  return (
    <Box testID="item" focusable>
      <Text color={focused ? "green" : undefined}>
        {focused ? "> " : "  "}
        {label}
      </Text>
    </Box>
  )
}

Return Value

PropertyTypeDescription
focusedbooleanWhether this component currently has focus
focus() => voidProgrammatically focus this component
blur() => voidProgrammatically blur this component
focusOrigin"keyboard" | "mouse" | "programmatic" | nullHow focus was acquired

useFocusWithin

Returns whether any descendant of the specified Box (by testID) has focus.

Usage

tsx
function Sidebar() {
  const hasFocus = useFocusWithin("sidebar")

  return (
    <Box testID="sidebar" borderColor={hasFocus ? "blue" : "gray"}>
      <FocusableItem testID="item1" />
      <FocusableItem testID="item2" />
    </Box>
  )
}

Parameters

ParameterTypeDescription
testIDstringThe testID of the Box to monitor for focus

Return Value

TypeDescription
booleanWhether any descendant of the specified Box has focus

useFocusManager

Access the focus manager for programmatic focus control across all focusable components.

Usage

tsx
function App() {
  const { activeId, focused, focusNext, focusPrev, blur } = useFocusManager()

  return (
    <Box flexDirection="column">
      <Text>Active: {activeId ?? "none"}</Text>
      <FocusableItem label="First" />
      <FocusableItem label="Second" />
      <FocusableItem label="Third" />
    </Box>
  )
}

Return Value

PropertyTypeDescription
activeIdstring | nulltestID of the currently focused node
activeElementInkxNode | nullThe currently focused node
focusedbooleanWhether any node has focus
focus(id: string) => voidFocus a specific component by testID
focusNext() => voidFocus the next focusable component
focusPrev() => voidFocus the previous focusable component
blur() => voidClear focus from all components

Box Focus Props

Focus behavior is configured via props on <Box>:

tsx
<Box focusable>           {/* Can receive focus */}
<Box focusable autoFocus> {/* Focus on mount */}
<Box focusScope>          {/* Isolated Tab cycle within subtree */}
<Box onFocus={handler}>   {/* Focus event (bubbles) */}
<Box onBlur={handler}>    {/* Blur event (bubbles) */}
<Box onKeyDown={handler}> {/* Key event dispatched to focused node (bubbles) */}
PropTypeDefaultDescription
focusablebooleanfalseNode can receive focus
autoFocusbooleanfalseFocus this node on mount
focusScopebooleanfalseTab cycles within this subtree
nextFocusUpstringtestID to focus on Up arrow (explicit override)
nextFocusDownstringtestID to focus on Down arrow
nextFocusLeftstringtestID to focus on Left arrow
nextFocusRightstringtestID to focus on Right arrow
onFocusfunctionCalled when this node gains focus
onBlurfunctionCalled when this node loses focus
onKeyDownfunctionKey event handler (bubble phase)

Examples

Tab Navigation

tsx
function Button({ label }: { label: string }) {
  const { focused } = useFocusable()

  return (
    <Box testID={label} focusable borderStyle={focused ? "double" : "single"}>
      <Text inverse={focused}>{label}</Text>
    </Box>
  )
}

function Form() {
  return (
    <Box flexDirection="column" gap={1}>
      <Button label="Submit" />
      <Button label="Cancel" />
      <Button label="Help" />
    </Box>
  )
}

Auto-Focus

tsx
function SearchInput() {
  const { focused } = useFocusable()

  return (
    <Box testID="search" focusable autoFocus borderStyle={focused ? "double" : "single"}>
      <Text>Search: </Text>
      <Text inverse={focused}>_</Text>
    </Box>
  )
}

Focus Scopes

tsx
function Dialog() {
  return (
    <Box testID="dialog" focusScope borderStyle="double">
      {/* Tab cycles only within this dialog */}
      <Button label="OK" />
      <Button label="Cancel" />
    </Box>
  )
}

Focus by ID

tsx
function Navigation() {
  const { focus } = useFocusManager()

  useInput((input) => {
    if (input === "1") focus("first")
    if (input === "2") focus("second")
    if (input === "3") focus("third")
  })

  return (
    <Box flexDirection="column">
      <FocusableItem testID="first" label="First (1)" />
      <FocusableItem testID="second" label="Second (2)" />
      <FocusableItem testID="third" label="Third (3)" />
    </Box>
  )
}

function FocusableItem({ testID, label }: { testID: string; label: string }) {
  const { focused } = useFocusable()

  return (
    <Box testID={testID} focusable>
      <Text inverse={focused}>{label}</Text>
    </Box>
  )
}

Focus Within for Panels

tsx
function Panel({ id, children }: { id: string; children: React.ReactNode }) {
  const hasFocus = useFocusWithin(id)

  return (
    <Box testID={id} borderColor={hasFocus ? "cyan" : "gray"}>
      {children}
    </Box>
  )
}

function Layout() {
  return (
    <Box flexDirection="row">
      <Panel id="sidebar">
        <FocusableItem testID="nav1" label="Nav 1" />
        <FocusableItem testID="nav2" label="Nav 2" />
      </Panel>
      <Panel id="main">
        <FocusableItem testID="content1" label="Content 1" />
        <FocusableItem testID="content2" label="Content 2" />
      </Panel>
    </Box>
  )
}

Action on Focus

tsx
function MenuItem({ label, onSelect }: { label: string; onSelect: () => void }) {
  const { focused } = useFocusable()

  return (
    <Box
      testID={label}
      focusable
      onKeyDown={(e) => {
        if (e.key === "Enter") onSelect()
      }}
    >
      <Text color={focused ? "cyan" : undefined}>
        {focused ? "> " : "  "}
        {label}
      </Text>
    </Box>
  )
}

Notes

  • Focus is managed by a tree-based FocusManager that operates on the InkxNode render tree
  • Tab moves forward through focusable nodes, Shift+Tab moves backward
  • focusScope creates an isolated focus cycle (Tab does not leave the subtree)
  • autoFocus on a Box focuses it when the component mounts
  • testID identifies nodes for programmatic focus and useFocusWithin
  • Click-to-focus is automatic when mouse events are enabled
  • Directional navigation (nextFocusUp, etc.) allows explicit spatial focus movement

Released under the MIT License.