Skip to content

Focus Hooks ​

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

Import ​

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

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
activeElementSilveryNode | 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
nextFocusUpstring—testID to focus on Up arrow (explicit override)
nextFocusDownstring—testID to focus on Down arrow
nextFocusLeftstring—testID to focus on Left arrow
nextFocusRightstring—testID to focus on Right arrow
onFocusfunction—Called when this node gains focus
onBlurfunction—Called when this node loses focus
onKeyDownfunction—Key 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 SilveryNode 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