Hooks

Reference for all React hooks in @visulima/tui/react: useInput, usePaste, useClipboard, useKeyBindings, useTextBuffer, useTextSelection, useColorBlindness, useConsoleCapture, useAnimation, useTimer, useStopwatch, useScrollAcceleration, useTerminalPalette, useTreeViewState, useTreeView, useMouse, useTextInput, useCursor, useScrollable, useFocus, useApp, and more

Hooks

Use these hooks inside components rendered with render(). Hooks that rely on the full app event emitter are marked as render()-mode only.

import { useApp, useCursor, useFocus, useFocusManager, useInput, useMouse, usePaste, useScrollable, useTextInput } from "@visulima/tui/react";

useInput

Subscribe to typed characters and key events.

Bracketed paste payloads belong to usePaste. If no usePaste handler is active, paste text is still forwarded to useInput for backward compatibility, but that fallback is deprecated and will be removed in the next major version.

useInput((input, key) => {
    if (key.upArrow) moveUp();
    if (key.downArrow) moveDown();
    if (key.return) submit();
    if (key.escape) cancel();
    if (key.ctrl && input === "c") exit();
});

key Fields

FieldType
upArrowboolean
downArrowboolean
leftArrowboolean
rightArrowboolean
returnboolean
backspaceboolean
deleteboolean
pageUpboolean
pageDownboolean
homeboolean
endboolean
tabboolean
shiftboolean
escapeboolean
ctrlboolean
metaboolean

usePaste

Subscribe to bracketed paste events.

usePaste((text) => {
    handlePaste(text);
});

usePaste is the dedicated API for paste blobs. When at least one active usePaste listener exists, pasted text is routed to usePaste and not forwarded to useInput. If no paste listener is active, paste still falls back to useInput, but that fallback is deprecated and will be removed in the next major version.

Options

usePaste(handler, { isActive: false });
OptionTypeDefault
isActivebooleantrue

useClipboard

Copy text to the system clipboard via OSC 52 terminal escape sequences. Works in terminals that support OSC 52 (Alacritty, Ghostty, Kitty, WezTerm, iTerm2, xterm, foot, Windows Terminal, etc.).

import { useClipboard } from "@visulima/tui";

const { copy, isSupported } = useClipboard();
copy("Hello, clipboard!");

Options

const { copy } = useClipboard({ target: "p" }); // primary selection (X11)
OptionTypeDefaultDescription
target"c" | "p" | "s""c"Clipboard target (c=clipboard, p=primary, s=secondary)

Return Value

FieldTypeDescription
copy(text: string) => voidCopy text; no-op if terminal is unsupported
isSupportedbooleanWhether the current terminal supports OSC 52

Low-level utilities

For use outside React components:

import { writeOsc52, clearOsc52, isOsc52Supported } from "@visulima/tui";

if (isOsc52Supported()) {
    writeOsc52(process.stdout, "text to copy", "c");
}

useTextBuffer

Manages a multi-line text buffer with cursor, selection, undo/redo. This is the data model behind the <Textarea> component, exposed for custom editor implementations.

import { useTextBuffer } from "@visulima/tui";

const buffer = useTextBuffer("initial\ntext");

buffer.insert("hello");
buffer.newline();
buffer.moveCursor("up");
buffer.undo();

Return Value

FieldTypeDescription
linesReadonlyArray<string>Current lines of text
cursor{ line: number, col: number }Current cursor position
anchor{ line: number, col: number } | nullSelection anchor (null if none)
valuestringJoined text (lines joined by \n)
insert(text: string) => voidInsert text at cursor
newline() => voidInsert a newline
deleteBack() => voidBackspace
deleteForward() => voidDelete key
deleteLine() => voidDelete entire current line
deleteToLineEnd() => voidCtrl+K
deleteToLineStart() => voidCtrl+U
deleteWord() => voidCtrl+W
moveCursor(dir, selecting?) => voidMove cursor (up/down/left/right)
moveToLineStart(selecting?) => voidHome
moveToLineEnd(selecting?) => voidEnd
moveToStart(selecting?) => voidStart of buffer
moveToEnd(selecting?) => voidEnd of buffer
selectAll() => voidSelect all text
clearSelection() => voidClear the selection
getSelectedText() => stringGet selected text
hasSelection() => booleanWhether text is selected
deleteSelection() => voidDelete selected text
replaceSelection(text: string) => voidReplace selection with text
undo() => voidUndo last edit
redo() => voidRedo last undo
setValue(value: string) => voidReplace entire buffer (clears undo)

Undo coalesces rapid keystrokes within 300ms into a single entry. The undo stack holds up to 100 snapshots.

useTextSelection

Manages text selection on a DOM element using the existing Selection/Range API, with optional auto-copy to clipboard.

import { useTextSelection } from "@visulima/tui";

const ref = useRef(null);
const { selectedText, selectAll, clearSelection } = useTextSelection(ref, {
    copyOnSelect: true,
    onSelectionChange: (text) => console.log("Selected:", text),
});

return (
    <Box ref={ref}>
        <Text>Selectable content</Text>
    </Box>
);

Options

OptionTypeDefaultDescription
isActivebooleantrueEnable/disable text selection
copyOnSelectbooleanfalseAuto-copy to clipboard on selection
onSelectionChange(text: string) => voidCalled when selected text changes

Return Value

FieldTypeDescription
selectionSelectionThe underlying Selection instance
selectedTextstringCurrently selected text
selectAll() => voidSelect all content in the element
clearSelection() => voidClear the current selection

useColorBlindness

Simulate or compensate for color vision deficiency. Returns a transformColor function that maps hex colors through a color blindness matrix.

import { useColorBlindness } from "@visulima/tui";

const { transformColor } = useColorBlindness({ mode: "deuteranopia" });
<Text color={transformColor("#ff0000")}>This text</Text>;
OptionTypeDefault
mode"protanopia" | "deuteranopia" | "tritanopia" | "achromatopsia" | "none""none"
compensatebooleanfalse

Simulation mode shows how colors appear to affected users. Compensation mode shifts colors to a distinguishable range. Low-level utilities (applyColorMatrix, COLOR_BLINDNESS_SIMULATION, COLOR_BLINDNESS_COMPENSATION) are also exported for direct use.

useConsoleCapture

Capture console.log/warn/error/info/debug output into a React state buffer. Used by the <ConsoleOverlay> component.

import { useConsoleCapture } from "@visulima/tui";

const { entries, clear } = useConsoleCapture({ maxEntries: 100 });
OptionTypeDefault
maxEntriesnumber200
filterConsoleLevel[]["log", "info", "warn", "error", "debug"]
isActivebooleantrue

Each entry has id, level, message, and timestamp.

Note: Only one active useConsoleCapture instance should exist at a time. The <ConsoleOverlay> component uses this hook internally — do not use both simultaneously unless the hook has isActive: false.

useScrollAcceleration

macOS-style scroll physics with momentum. Tracks scroll event velocity and applies exponential acceleration when events arrive rapidly.

import { useScrollAcceleration } from "@visulima/tui";

const { handleScroll } = useScrollAcceleration({
    onScroll: (delta) => scrollViewRef.current?.scrollBy(delta),
});
OptionTypeDefault
accelerationnumber1.5
decayRatenumber (0-1)0.92
maxVelocitynumber20
isActivebooleantrue
onScroll(delta: number) => void

Returns handleScroll(direction), velocity, and isCoasting.

useScrollInput

Wire keyboard input to scroll actions. Used internally by ScrollView/ScrollList when keyboard prop is enabled, but also available standalone for custom scroll containers.

import { useScrollInput } from "@visulima/tui";

const { isFocused } = useScrollInput({
    scrollBy: (delta) => ref.current?.scrollBy(delta),
    scrollToTop: () => ref.current?.scrollToTop(),
    scrollToBottom: () => ref.current?.scrollToBottom(),
    viewportHeight: 20,
    vimBindings: true,
});
OptionTypeDefault
scrollBy(delta: number) => voidrequired
scrollToTop() => voidrequired
scrollToBottom() => voidrequired
viewportHeightnumberrequired
isActivebooleantrue
vimBindingsbooleanfalse

Returns { isFocused }.

createLinkedScrollGroup

Synchronize scroll position across multiple ScrollView instances.

import { createLinkedScrollGroup } from "@visulima/tui";

const group = createLinkedScrollGroup();

// In each linked component:
const { ref, onScroll } = group.useLinkedScroll();
// Attach ref and onScroll to your ScrollView

Returns a LinkedScrollGroup with a useLinkedScroll() hook that provides { ref, onScroll } for each consumer.

useTerminalPalette

Query the terminal emulator for its current color palette using OSC escape sequences.

import { useTerminalPalette } from "@visulima/tui";

const { palette, isLoading, isSupported } = useTerminalPalette();
if (palette?.background) {
    console.log("Terminal background:", palette.background);
}

Returns palette (with foreground, background, cursor, and colors array of 16 ANSI colors as hex strings), isLoading, and isSupported. Low-level queryTerminalPalette() and isTerminalPaletteQuerySupported() are also exported.

useMouse (native renderer only)

Low-level callback hook for mouse events. Subscribe to every mouse event with full modifier info.

useMouse((event) => {
    // event.button: left | right | middle | scrollUp | scrollDown
    // event.x, event.y are 0-based terminal coordinates
    if (event.button === "left") onClick(event.x, event.y);
});

Event Shape

FieldTypeNotes
xnumber0-based column
ynumber0-based row
buttonstringleft/right/middle/scrollUp/scrollDown
shiftbooleanmodifier
ctrlbooleanmodifier
metabooleanmodifier

For higher-level mouse hooks with element hover/click detection, see Mouse Support (Ink layer, imported from @visulima/tui).

useTextInput (native renderer only)

Managed text input with cursor and editing shortcuts.

const { value, cursor, clear } = useTextInput({
    onSubmit: (v) => send(v),
});

return (
    <Text>
        {value.slice(0, cursor)}
        <Text inverse>{value[cursor] ?? " "}</Text>
        {value.slice(cursor + 1)}
    </Text>
);

Options

OptionTypeDefault
initialValuestring''
onSubmit(v: string) => void
onChange(v: string) => void
isActivebooleantrue

Return Value

FieldType
valuestring
cursornumber
setValue(v: string) => void
clear() => void

Supported editing keys: arrows, Home/End, Backspace/Delete, Ctrl+A/E/U/K/W, Enter, paste.

useScrollable (native renderer only)

Virtual scrolling state for fixed-height viewports.

const viewportHeight = 20;
const scroll = useScrollable({
    viewportHeight,
    contentHeight: items.length,
});

useInput((_input, key) => {
    if (key.upArrow) scroll.scrollUp();
    if (key.downArrow) scroll.scrollDown();
    if (key.pageUp) scroll.scrollBy(-10);
    if (key.pageDown) scroll.scrollBy(10);
    if (key.home) scroll.scrollToTop();
    if (key.end) scroll.scrollToBottom();
});

const visible = items.slice(scroll.offset, scroll.offset + viewportHeight);

Return Value

offset, scrollUp, scrollDown, scrollBy, scrollToTop, scrollToBottom, atTop, atBottom.

useFocus

Register a focusable component.

const { isFocused, focus } = useFocus({ id: "search", autoFocus: true });

Tab/Shift+Tab focus cycling is wired automatically by render().

useFocusManager

Programmatic focus control.

const { focusNext, focusPrevious, focus, activeId, enableFocus, disableFocus } = useFocusManager();

useApp

Lifecycle controls.

const { exit, quit } = useApp();

exit() and quit() are equivalent.

useWindowSize

Returns { columns, rows } and updates on terminal resize.

const { columns, rows } = useWindowSize();

This hook relies on the full app event emitter and is intended for render() mode.

useStdout / useStderr

Write outside the rendered frame.

const { write } = useStdout();
const { write: writeErr } = useStderr();

write("log line\n");
writeErr("warning\n");

useStdin

Access stdin and raw mode controls.

const { stdin, setRawMode, isRawModeSupported } = useStdin();

useBoxMetrics

Read computed layout metrics from a Box ref.

const ref = useRef(null);
const { width, height, left, top, hasMeasured } = useBoxMetrics(ref);

return <Box ref={ref}>...</Box>;

Returns zeroed metrics until first layout pass.

This hook relies on the full app event emitter and is intended for render() mode.

measureElement

Imperative element measurement. Returns position and dimensions.

const { x, y, width, height } = measureElement(ref.current);

Additional measurement utilities are available for advanced use cases:

import {
    getBoundingBox, // scroll-aware absolute position
    getInnerWidth, // width excluding borders
    getInnerHeight, // height excluding borders
    getScrollHeight, // total scrollable content height
    getScrollWidth, // total scrollable content width
    getScrollTop, // current vertical scroll position
    getScrollLeft, // current horizontal scroll position
    ResizeObserver, // observe element size changes
} from "@visulima/tui";

// ResizeObserver example
const observer = new ResizeObserver((entries) => {
    for (const entry of entries) {
        console.log(`Size changed: ${entry.contentRect.width}x${entry.contentRect.height}`);
    }
});
observer.observe(ref.current);

useCursor

Imperative cursor positioning. Use this when you need precise control over cursor placement, for example with IME (Input Method Editor) support where you need to account for wide characters.

const { setCursorPosition } = useCursor();

const prompt = "> ";
setCursorPosition({ x: stringWidth(prompt + text), y: 1 });

return (
    <Box flexDirection="column">
        <Text>Header</Text>
        <Text>
            {prompt}
            {text}
        </Text>
    </Box>
);

setCursorPosition

ArgumentTypeDescription
position{ x: number, y: number } or undefinedAbsolute position, or undefined to hide

Notes:

  • x and y are 0-based coordinates relative to the top-left of the rendered output.
  • Use stringWidth() from @visulima/string for correct column calculation with wide characters.
  • The cursor is automatically hidden when the component using useCursor unmounts.
  • For most text input use cases, prefer the declarative <Cursor /> component instead.

useAnimation

Drive frame-based or time-based animations. Returns a frame counter, elapsed time, frame delta, and a reset function. All animations share a single timer internally, so multiple useAnimation calls consolidate into one render cycle instead of spawning independent timers.

import { useAnimation, Text } from "@visulima/tui";

const Spinner = () => {
    const { frame } = useAnimation({ interval: 80 });
    const characters = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];

    return <Text>{characters[frame % characters.length]}</Text>;
};

Options

OptionTypeDefaultDescription
intervalnumber100Time between ticks in milliseconds.
isActivebooleantrueWhether the animation is running. When set to false, the animation stops. When toggled back to true, values reset to 0.

Return Value

FieldTypeDescription
framenumberDiscrete counter that increments by 1 each interval. Useful for indexed sequences like spinner frames.
timenumberTotal elapsed milliseconds since the animation started or was last reset. Useful for continuous math-based animations like sine waves.
deltanumberMilliseconds since the previous rendered tick. Accounts for throttled renders. Useful for physics-based or velocity-driven motion.
reset() => voidResets frame, time, and delta to 0 and restarts timing from the current moment.

Examples

Continuous sine-wave animation using time

const Pulse = () => {
    const { time } = useAnimation({ interval: 50 });
    const brightness = Math.sin((time / 1000) * Math.PI * 2);
    const width = Math.round((brightness + 1) * 10);

    return <Text color="cyan">{"█".repeat(width)}</Text>;
};

Pausable animation using isActive

import { useState } from "react";
import { Text, useAnimation, useInput } from "@visulima/tui";

const PausableCounter = () => {
    const [paused, setPaused] = useState(false);
    const { frame } = useAnimation({ interval: 100, isActive: !paused });

    useInput((input) => {
        if (input === " ") setPaused((prev) => !prev);
    });

    return (
        <Text>
            Frame: {frame} {paused ? "(paused)" : ""}
        </Text>
    );
};

Resettable one-shot animation using reset

import { Text, useAnimation, useInput } from "@visulima/tui";

const Flash = () => {
    const { frame, reset } = useAnimation({ interval: 100 });

    useInput((input) => {
        if (input === "r") reset();
    });

    return <Text>{frame < 10 ? "Flash!" : "Press r to flash again"}</Text>;
};

Physics-based motion using delta

import { useRef } from "react";
import { Box, Text, useAnimation } from "@visulima/tui";

const BouncingDot = () => {
    const posRef = useRef(0);
    const velRef = useRef(0.05);
    const { delta } = useAnimation({ interval: 16 });

    posRef.current += velRef.current * delta;

    if (posRef.current > 40 || posRef.current < 0) {
        velRef.current *= -1;
        posRef.current = Math.max(0, Math.min(40, posRef.current));
    }

    return <Text>{" ".repeat(Math.round(posRef.current))}●</Text>;
};

Multiple animations at different speeds

Multiple calls share one internal timer. The scheduler wakes at the earliest deadline and lets slower animations skip that tick.

const Dashboard = () => {
    const { frame: fast } = useAnimation({ interval: 80 });
    const { frame: slow } = useAnimation({ interval: 500 });

    const spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
    const dots = [".  ", ".. ", "..."];

    return (
        <Box flexDirection="column">
            <Text>
                {spinner[fast % spinner.length]} Loading
                {dots[slow % dots.length]}
            </Text>
        </Box>
    );
};

Interaction with maxFps

The maxFps option on render() controls how often Ink flushes frames to the terminal. useAnimation respects this throttle — when maxFps is low (e.g., maxFps: 5), animation callbacks are coalesced and the delta value reflects the actual time between rendered frames, not the interval. The frame and time values are always computed from elapsed wall-clock time, so they remain accurate regardless of throttling.

useTimer

A countdown timer hook built on useAnimation. Returns the remaining time and control methods.

import { useTimer } from "@visulima/tui";

const { remaining, isRunning, isFinished, start, stop, toggle, reset } = useTimer({
    duration: 60_000,
    interval: 1000,
    onTimeout: () => console.log("Time's up!"),
});

Options

OptionTypeDefaultDescription
durationnumber(required)Countdown duration in milliseconds
intervalnumber1000Tick frequency in milliseconds
autoStartbooleanfalseStart counting immediately
onTimeout() => voidCalled once when time reaches zero

Return Value

FieldTypeDescription
remainingnumberMilliseconds remaining (clamped to >= 0)
isRunningbooleanWhether the timer is counting down
isFinishedbooleanWhether the countdown has reached zero
start() => voidStart the countdown
stop() => voidPause the countdown
toggle() => voidToggle between running and stopped
reset() => voidReset to original duration and stop

For a visual component, see <Timer>.

useStopwatch

A count-up stopwatch hook built on useAnimation. Returns elapsed time, control methods, and lap functionality.

import { useStopwatch } from "@visulima/tui";

const { elapsed, isRunning, start, stop, toggle, reset, lap, laps } = useStopwatch({
    interval: 100,
    autoStart: true,
});

Options

OptionTypeDefaultDescription
intervalnumber1000Tick frequency in milliseconds
autoStartbooleanfalseStart counting immediately

Return Value

FieldTypeDescription
elapsednumberMilliseconds elapsed since start/reset
isRunningbooleanWhether the stopwatch is running
start() => voidStart counting
stop() => voidPause counting
toggle() => voidToggle between running and stopped
reset() => voidReset to zero, clear laps, and stop
lap() => numberCapture current elapsed time; returns it
lapsnumber[]Accumulated lap times

For a visual component, see <Stopwatch>.

useKeyBindings

Declarative keybinding hook. Registers multiple key bindings with handlers and returns the enabled bindings for display in a <Help> component.

import { useKeyBindings, Help } from "@visulima/tui";

const { bindings } = useKeyBindings([
    { binding: { key: "q", description: "Quit" }, handler: () => exit() },
    { binding: { key: ["upArrow", "k"], description: "Move up", group: "Navigation" }, handler: () => moveUp() },
    { binding: { key: "ctrl+s", description: "Save" }, handler: () => save() },
    { binding: { key: "escape", description: "Cancel", enabled: false }, handler: () => cancel() },
]);

<Help bindings={bindings} />

Key Binding Format

The key field supports:

  • Single character: "q", "a", " " (space)
  • Special key name: "return", "escape", "upArrow", "leftArrow", "backspace", "tab", "pageUp", "pageDown", "home", "end", "delete"
  • Modifier combo: "ctrl+c", "meta+s", "shift+tab" (modifier + key)
  • Array: ["upArrow", "k"] — matches any of the listed keys

KeyBinding type

type KeyBinding = {
    key: string | readonly string[];
    description: string;
    enabled?: boolean;   // default: true
    group?: string;      // for Help grouping
};

Options

OptionTypeDefaultDescription
isActivebooleantrueWhether input is captured

Return Value

FieldTypeDescription
bindingsKeyBinding[]Enabled bindings, for passing to <Help>

Disabled bindings (enabled: false) are excluded from both the returned array and keyboard matching.

useIsScreenReaderEnabled

Returns whether screen reader mode is active. When enabled, components render aria-label text instead of visual content.

const isEnabled = useIsScreenReaderEnabled();

Screen reader mode is enabled by either:

  • Passing isScreenReaderEnabled: true to render()
  • Setting the INK_SCREEN_READER=true environment variable

Defaults to false. This matches upstream Ink behavior — there is no automatic detection of screen readers.

useTreeViewState

State management hook for the <TreeView> component. Use this for headless tree view implementations where you want full control over rendering.

import { useTreeViewState } from "@visulima/tui";

const state = useTreeViewState({
    data: treeNodes,
    selectionMode: "single",
    defaultExpanded: "all",
    visibleNodeCount: 20,
    onFocusChange: (id) => console.log("focused:", id),
    onExpandChange: (ids) => console.log("expanded:", ids),
    onSelectChange: (ids) => console.log("selected:", ids),
});

Returns a TreeViewState<T> object with:

  • Read-only state: focusedId, expandedIds, selectedIds, viewportNodes, visibleCount, hasScrollUp, hasScrollDown, loadingIds, nodeMap
  • Actions: focusNext(), focusPrevious(), focusFirst(), focusLast(), expand(), collapse(), expandNode(id), collapseNode(id), toggleExpanded(), expandAll(), collapseAll(), select(), focusParent(), focusFirstChild(), setLoading(id, bool), insertChildren(parentId, children)

useTreeView

Keyboard input hook for the tree view. Binds arrow keys, Enter, Space, Home, and End to the tree view state actions. Use alongside useTreeViewState for headless implementations.

import { useTreeViewState, useTreeView } from "@visulima/tui";

const state = useTreeViewState({ data, selectionMode: "none" });

useTreeView({
    state,
    selectionMode: "none",
    isDisabled: false,
    loadChildren: async (node) => fetchChildren(node.id),
    onLoadError: (nodeId, err) => console.error(err),
});
PropTypeDefaultDescription
stateTreeViewState<T>requiredState from useTreeViewState
selectionMode"none" | "single" | "multiple"requiredDetermines Enter/Space behavior
isDisabledbooleanfalseIgnores all keyboard input
loadChildren(node: TreeNode<T>) => Promise<TreeNode<T>[]>Async child loader
onLoadError(nodeId: string, error: Error) => voidError callback for async loading
Support

Contribute to our work and keep us going

Community is the heart of open source. The success of our packages wouldn't be possible without the incredible contributions of users, testers, and developers who collaborate with us every day.Want to get involved? Here are some tips on how you can make a meaningful impact on our open source projects.

Ready to help us out?

Be sure to check out the package's contribution guidelines first. They'll walk you through the process on how to properly submit an issue or pull request to our repositories.

Submit a pull request

Found something to improve? Fork the repo, make your changes, and open a PR. We review every contribution and provide feedback to help you get merged.

Good first issues

Simple issues suited for people new to open source development, and often a good place to start working on a package.
View good first issues