Skip to content

useInput

Handle keyboard input in your app.

Import

tsx
import { useInput } from "inkx"

Usage

tsx
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>
}

Signature

tsx
useInput(
  callback: (input: string, key: Key) => void,
  options?: { isActive?: boolean }
)

Parameters

ParameterTypeDescription
callback(input: string, key: Key) => voidCalled on each keypress
options.isActivebooleanWhether to listen for input (default: true)

Key Object

PropertyTypeDescription
upArrowbooleanUp arrow key
downArrowbooleanDown arrow key
leftArrowbooleanLeft arrow key
rightArrowbooleanRight arrow key
returnbooleanEnter/Return key
escapebooleanEscape key
ctrlbooleanControl modifier
shiftbooleanShift modifier
metabooleanMeta/Command modifier
tabbooleanTab key
backspacebooleanBackspace key
deletebooleanDelete key
pageUpbooleanPage Up key
pageDownbooleanPage Down key

Examples

tsx
function Menu({ items }: { items: string[] }) {
  const [selected, setSelected] = useState(0)

  useInput((input, key) => {
    if (key.upArrow) {
      setSelected((s) => Math.max(0, s - 1))
    }
    if (key.downArrow) {
      setSelected((s) => Math.min(items.length - 1, s + 1))
    }
    if (key.return) {
      console.log(`Selected: ${items[selected]}`)
    }
  })

  return (
    <Box flexDirection="column">
      {items.map((item, i) => (
        <Text key={i} inverse={i === selected}>
          {i === selected ? "> " : "  "}
          {item}
        </Text>
      ))}
    </Box>
  )
}

Keyboard Shortcuts

tsx
function App() {
  const { exit } = useApp()

  useInput((input, key) => {
    // Ctrl+C to exit
    if (input === "c" && key.ctrl) {
      exit()
    }

    // Ctrl+S to save
    if (input === "s" && key.ctrl) {
      save()
    }

    // 'h' for help
    if (input === "h") {
      showHelp()
    }
  })

  return <Text>Press h for help, Ctrl+C to quit</Text>
}

Conditional Input

tsx
function Modal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
  // Only listen when modal is open
  useInput(
    (input, key) => {
      if (key.escape) onClose()
    },
    { isActive: isOpen },
  )

  if (!isOpen) return null

  return (
    <Box borderStyle="double">
      <Text>Press Escape to close</Text>
    </Box>
  )
}

Text Input

tsx
function TextInput({ value, onChange }: { value: string; onChange: (v: string) => void }) {
  useInput((input, key) => {
    if (key.backspace) {
      onChange(value.slice(0, -1))
    } else if (key.return) {
      // Submit
    } else if (input && !key.ctrl && !key.meta) {
      onChange(value + input)
    }
  })

  return (
    <Box>
      <Text>{value}</Text>
      <Text inverse> </Text>
    </Box>
  )
}

Vi-style Movements

tsx
function ViNavigation() {
  const [position, setPosition] = useState({ x: 0, y: 0 })

  useInput((input) => {
    switch (input) {
      case "h": // Left
        setPosition((p) => ({ ...p, x: Math.max(0, p.x - 1) }))
        break
      case "j": // Down
        setPosition((p) => ({ ...p, y: p.y + 1 }))
        break
      case "k": // Up
        setPosition((p) => ({ ...p, y: Math.max(0, p.y - 1) }))
        break
      case "l": // Right
        setPosition((p) => ({ ...p, x: p.x + 1 }))
        break
    }
  })

  return (
    <Text>
      Position: ({position.x}, {position.y})
    </Text>
  )
}

Notes

  • Input is only captured when the app is in raw mode (default for render())
  • Multiple useInput hooks can be active simultaneously
  • Use isActive: false to temporarily disable input handling

Released under the MIT License.