Skip to content

useInput

useInput gives your component access to raw keyboard input. Your handler receives the pressed key as a string and a KeyEvent object with modifier flags.

Use useInput when you need custom per-key logic. For a list of named bindings, see useKeymap. For arrow-key list navigation, see useKeyboardNavigation below.

Installation

·CODE
npm install @termuijs/jsx

Signature

·CODE
useInput(handler: (key: string, event: KeyEvent) => void): void
ParameterTypeDescription
handler(key: string, event: KeyEvent) => voidCalled on every keypress

The handler runs on each keypress while the component is mounted. No cleanup is needed; TermUI removes it automatically on unmount.

KeyEvent properties

PropertyTypeDescription
keystringKey name, e.g. 'a', 'up', 'enter', 'escape'
ctrlbooleanTrue if Ctrl was held
altbooleanTrue if Alt/Option was held
shiftbooleanTrue if Shift was held
sequencestringRaw terminal escape sequence

Special keys use lowercase names: 'up', 'down', 'left', 'right', 'enter', 'escape', 'backspace', 'tab', 'space', 'pageup', 'pagedown', 'home', 'end', 'delete'.

Basic usage

·CODE
import { useInput } from '@termuijs/jsx'
import { Text } from '@termuijs/widgets'

function QuitOnQ() {
    useInput((key) => {
        if (key === 'q') process.exit(0)
    })

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

Modifier combos

Check the event argument for modifier flags:

·CODE
import { useInput } from '@termuijs/jsx'
import { Box, Text } from '@termuijs/widgets'

function SearchPanel() {
    useInput((key, event) => {
        if (key === 'f' && event.ctrl) openSearch()
        if (key === 'r' && event.ctrl) refresh()
        if (key === 'escape')           closePanel()
    })

    return (
        <Box flexDirection="column">
            <Text dim>ctrl+f — search, ctrl+r — refresh, esc — close</Text>
        </Box>
    )
}

Vim-style bindings

·CODE
import { useState } from '@termuijs/jsx'
import { useInput } from '@termuijs/jsx'
import { Text } from '@termuijs/widgets'

type Mode = 'normal' | 'insert'

function VimEditor() {
    const [mode, setMode] = useState<Mode>('normal')
    const [text, setText] = useState('')

    useInput((key, event) => {
        if (mode === 'normal') {
            if (key === 'i') { setMode('insert'); return }
            if (key === 'q') process.exit(0)
        }

        if (mode === 'insert') {
            if (key === 'escape') { setMode('normal'); return }
            if (key === 'backspace') {
                setText((t) => t.slice(0, -1))
            } else if (!event.ctrl && !event.alt && key.length === 1) {
                setText((t) => t + key)
            }
        }
    })

    return (
        <Box flexDirection="column">
            <Text>{text || ' '}</Text>
            <Text dim>-- {mode.toUpperCase()} --</Text>
        </Box>
    )
}

Multiple useInput calls

You can call useInput more than once in the same component. Both handlers fire on every keypress, in call order:

·CODE
function Panel() {
    useInput((key) => {
        if (key === 'q') process.exit(0)   // global quit
    })

    useInput((key) => {
        if (key === 'r') refresh()         // panel-level action
    })

    return <Box>...</Box>
}

Interaction with the focus system

useInput fires on every keypress regardless of focus. To scope input to a focused component, pair it with useFocus:

·CODE
import { useInput, useFocus } from '@termuijs/jsx'
import { Box } from '@termuijs/widgets'

function FocusableItem({ id }: { id: string }) {
    const { isFocused } = useFocus({ id })

    useInput((key) => {
        if (!isFocused) return    // ignore input when not focused
        if (key === 'enter') activate()
    })

    return <Box borderColor={isFocused ? 'blue' : undefined}>...</Box>
}

See the Focus page for the full focus system.


useKeyboardNavigation

useKeyboardNavigation wires standard list-navigation keys to a selected index. It calls useInput internally, so you do not need to add useInput yourself for arrow-key navigation.

Signature

·CODE
useKeyboardNavigation(options: KeyboardNavigationOptions): KeyboardNavigationResult

Options

OptionTypeDefaultDescription
itemCountnumberrequiredTotal number of items
loopbooleantrueWrap around at boundaries
pageSizenumber10Items to skip on PageUp/PageDown
onSelect(index: number) => void,Called when Enter is pressed

Returns

FieldTypeDescription
selectedIndexnumberCurrent selection (0-based)
setSelectedIndexsetterProgrammatically move the selection

Keys bound

KeyAction
upMove selection up by 1
downMove selection down by 1
homeJump to first item
endJump to last item
pageupMove up by pageSize
pagedownMove down by pageSize
enterCall onSelect with current index

Example

·CODE
import { useKeyboardNavigation } from '@termuijs/jsx'
import { List } from '@termuijs/widgets'

function FileList({ files }: { files: string[] }) {
    const { selectedIndex } = useKeyboardNavigation({
        itemCount: files.length,
        loop: false,
        onSelect: (i) => openFile(files[i]!),
    })

    return <List items={files} selectedIndex={selectedIndex} />
}

Clamping instead of wrapping

Set loop: false to stop at the first and last items:

·CODE
const { selectedIndex } = useKeyboardNavigation({
    itemCount: items.length,
    loop: false,
    onSelect: handleSelect,
})

Programmatic selection

Use setSelectedIndex to jump to a specific item from outside the hook:

·CODE
const { selectedIndex, setSelectedIndex } = useKeyboardNavigation({
    itemCount: results.length,
})

// Jump to top when search term changes
useEffect(() => {
    setSelectedIndex(0)
}, [searchTerm])

See also

  • useKeymap, declarative named key bindings
  • Focus, scope input to focused components
  • useMotion, skip animations based on user preference