Skip to content

Focus Management

TermUI ships four hooks for managing keyboard focus. Together they handle everything from basic input activation to fully accessible keyboard navigation.

Hooks overview

HookRole
useFocusManagerOwns focus state for a subtree, mount once near the root
useFocusReads and controls focus for a single widget
useFocusTrapConfines Tab/Shift+Tab to a list of IDs (for modals)
useKeyboardNavigationArrow key navigation for lists and menus

useFocusManager

Provides the focus context for all descendant components. Returns { focused, focus, blur, FocusContext }, wrap children in FocusContext.Provider to share state.

·CODE
import { useFocusManager } from '@termuijs/jsx'

function Form() {
    const { focused, focus, blur, FocusContext } = useFocusManager()

    return (
        <FocusContext.Provider value={{ focused, focus, blur }}>
            <NameField />
            <EmailField />
            <SubmitButton />
        </FocusContext.Provider>
    )
}

focused is the ID string of the currently focused widget, or null if nothing is focused.

API

PropertyTypeDescription
focused`string \null`
focus(id)(id: string) => voidSet focus to the given ID
blur()() => voidClear focus
FocusContextContext objectPass to Provider to share state with descendants

useFocus

Reads and controls focus for a single widget. Call inside any component that wants to participate in focus.

·CODE
import { useFocus } from '@termuijs/jsx'

function NameField() {
    const { isFocused, focus, blur } = useFocus({ id: 'name-field', autoFocus: true })

    return (
        <Box border={isFocused ? 'double' : 'single'} borderColor={isFocused ? 'cyan' : 'brightBlack'}>
            <TextInput placeholder="Name" />
        </Box>
    )
}

Options

OptionTypeDefaultDescription
idstringRequiredUnique ID within the focus manager
autoFocusbooleanfalseImmediately focus this widget on mount

Returns

PropertyTypeDescription
isFocusedbooleanWhether this widget currently has focus
focus()() => voidRequest focus
blur()() => voidRelease focus

useFocusTrap

Traps Tab and Shift+Tab within a list of widget IDs. When Tab is pressed on the last item it wraps to the first; Shift+Tab on the first wraps to the last.

Use inside modals, dialogs, or any overlay that should capture all keyboard navigation.

·CODE
import { useFocusTrap } from '@termuijs/jsx'

function ConfirmModal() {
    useFocusTrap(['modal-yes', 'modal-no'])

    return (
        <Box border="double">
            <Text>Are you sure?</Text>
            <FocusableButton id="modal-yes" label="Yes" />
            <FocusableButton id="modal-no" label="No" />
        </Box>
    )
}

The trap is removed automatically when the component unmounts.

API

·CODE
useFocusTrap(ids: string[]): void

ids should be the ordered list of focusable widget IDs. Tab cycles forward through the list; Shift+Tab cycles backward.


useKeyboardNavigation

Provides standard arrow-key navigation for lists and menus. Handles /, Home, End, PageUp, PageDown.

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

function Menu({ items }: { items: string[] }) {
    const { selectedIndex } = useKeyboardNavigation({
        itemCount: items.length,
        loop: true,
        pageSize: 10,
    })

    return (
        <col>
            {items.map((item, i) => (
                <Text key={item} bold={i === selectedIndex}>
                    {i === selectedIndex ? '▶ ' : '  '}{item}
                </Text>
            ))}
        </col>
    )
}

Options

OptionTypeDefaultDescription
itemCountnumberRequiredTotal number of items to navigate
loopbooleantrueWrap around at list boundaries
pageSizenumber10How many items PageUp/PageDown skip
onSelect(index: number) => void-Called when Enter is pressed

Returns

PropertyTypeDescription
selectedIndexnumberCurrent position in the list
setSelectedIndex(i: number) => voidProgrammatically move the selection

Full example: accessible form

·CODE
function LoginForm() {
    const { focused, focus, blur, FocusContext } = useFocusManager()
    useFocusTrap(['login-user', 'login-pass', 'login-submit'])

    return (
        <FocusContext.Provider value={{ focused, focus, blur }}>
            <Box border="single" padding={1} flexDirection="column">
                <Text bold>Sign In</Text>
                <LabeledInput id="login-user" label="Username" autoFocus />
                <LabeledInput id="login-pass" label="Password" password />
                <SubmitButton id="login-submit" />
            </Box>
        </FocusContext.Provider>
    )
}

See also

  • useKeymap, declarative key bindings per component
  • UI Inputs, PasswordInput, NumberInput, PathInput built on this focus system