Skip to content

Hooks ​

Silvery provides hooks for input handling, layout queries, terminal access, and interaction features.

Input hooks by intent ​

Choose the narrowest hook for the job:

IntentHook
Application commanduseHotkey
Several application commandsuseHotkeyMap
Typed and pasted textuseTextInput
Ink-compatible mixed handleruseInput
Release/modifier observationuseRawKeyEvent

The semantic hooks keep normalized key identity separate from produced text. In particular, bind the literal character "?" with useHotkey("?", handler); the low-level input value for that press is the normalized base key "/".

Interaction Feature Hooks ​

These hooks read state from interaction features registered in the CapabilityRegistry. The shipped run() runtime installs its mouse features automatically; explicitly composed runtimes must register the corresponding capability themselves.

useSelection ​

Reads the current text selection state from the SelectionFeature. It is available in mouse-enabled run() composition.

tsx
import { useSelection } from "silvery"

function SelectionIndicator() {
  const selection = useSelection()
  if (!selection?.active) return null
  return <Text>Selected: {selection.text}</Text>
}

Returns TerminalSelectionState | undefined — undefined when no selection feature is registered.

Legacy hooks

Legacy search hooks are no longer part of the public surface; use SearchProvider, useSearch(), and a registered semantic searchable. Other superseded interaction hooks remain compatibility-only. New selection code should use useSelection() and the capabilities installed by the shipped run() runtime.

useBoxRectDangerously ​

Returns the computed dimensions of the component's content area — width, height, and position.

tsx
import { Box, Text, useBoxRectDangerously } from "silvery"

function SizedBox() {
  const { width, height, x, y } = useBoxRectDangerously()

  return (
    <Box borderStyle="single">
      <Text>
        Size: {width}x{height}
      </Text>
      <Text>
        Position: ({x}, {y})
      </Text>
    </Box>
  )
}

Return Value ​

PropertyTypeDescription
widthnumberComputed width in characters
heightnumberComputed height in lines
xnumberX position from left edge
ynumberY position from top edge

First Render Behavior ​

On the first render, dimensions are { width: 0, height: 0, x: 0, y: 0 }. This is because layout hasn't been computed yet. The component will immediately re-render with correct values.

tsx
function Header() {
  const { width } = useBoxRectDangerously()

  // Guard against first render if needed
  if (width === 0) return null

  return <Text>{"=".repeat(width)}</Text>
}

In practice, both renders happen before the first paint, so this is usually invisible.

MeasuredBox primitive ​

For the common pattern of "render based on my own measured size," use the <MeasuredBox> primitive instead of writing the useBoxRectDangerously() + width > 0 ? <Inner /> : null dance by hand:

tsx
import { MeasuredBox } from "silvery"
;<MeasuredBox width="100%" flexDirection="column" alignItems="center">
  {({ width }) => <Banner availableWidth={width} />}
</MeasuredBox>

MeasuredBox defers rendering its render-prop children until the outer Box's measured width is non-zero — eliminating the width=0 sentinel frame.

useTerm ​

Access the Term instance for terminal capabilities and styling.

tsx
import { useTerm } from "silvery"

function ColoredOutput() {
  const term = useTerm()

  return (
    <Box>
      <Text>{term.green("Success!")} Operation completed.</Text>
      <Text>
        Terminal size: {term.columns}x{term.rows}
      </Text>
    </Box>
  )
}

Return Value ​

Returns the Term instance passed to render(). Provides:

Property/MethodDescription
columnsTerminal width
rowsTerminal height
hasColor()Check if terminal supports colors
Color methodsred(), green(), bold(), etc.

useInput ​

Handle the low-level Ink-compatible (input, key) tuple. Prefer useHotkey for commands and useTextInput for text in new code; see the useInput API reference for the normalized-key contract.

tsx
import { useInput } from "silvery"

function App() {
  const [count, setCount] = useState(0)

  useInput((input, key) => {
    if (key.upArrow) setCount((c) => c + 1)
    if (key.downArrow) setCount((c) => c - 1)
    if (input === "q") process.exit()
  })

  return <Text>Count: {count}</Text>
}

The low-level input value is normalized for physical key matching, while key.text is the produced character. See the useInput API reference for the complete signature, options, and Key fields.

useApp ​

Access app-level controls.

tsx
import { useApp } from "silvery"

function App() {
  const { exit, panic } = useApp()

  useInput((input) => {
    if (input === "q") exit()
    if (input === "P") panic("fatal provider invariant", { title: "my-app" })
  })

  return <Text>Press q to quit</Text>
}

Return Value ​

PropertyTypeDescription
exit(error?: Error) => voidExit the app
panic(reason: unknown, options?: PanicOptions) => voidRestore the terminal, then print a copyable fatal diagnostic to stderr

useStdout ​

Access stdout stream and dimensions.

tsx
import { useStdout } from "silvery"

function App() {
  const { stdout, write } = useStdout()

  return (
    <Text>
      Terminal: {stdout.columns}x{stdout.rows}
    </Text>
  )
}

Return Value ​

PropertyTypeDescription
stdoutNodeJS.WriteStreamstdout stream
write(data: string) => voidWrite directly to stdout

useFocusable ​

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

tsx
import { useFocusable, Box, Text } from "silvery"

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

  return (
    <Box testID={label} focusable borderStyle={focused ? "double" : "single"}>
      <Text>{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

Focus behavior is configured via Box props: focusable, autoFocus, focusScope, onFocus, onBlur, onKeyDown.

useFocusWithin ​

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

tsx
import { useFocusWithin } from "silvery"

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

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

useFocusManager ​

Access the focus manager for programmatic focus control.

tsx
import { useFocusManager } from "silvery"

function App() {
  const { activeId, focusNext, focusPrev, focus, 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 next focusable component
focusPrev() => voidFocus previous focusable component
blur() => voidClear focus from all components