Components

Reference for all React components in @visulima/tui: Alert, Badge, Box, Code, ConsoleOverlay, FilePicker, Text, BigText, ConfirmInput, DiffView, Gradient, Help, Link, Markdown, MultiSelect, Newline, OrderedList, Paginator, Slider, Spacer, Static, StatusMessage, Stopwatch, Tab, Tabs, Textarea, TextInput, Timer, Transform, TreeView, Spinner, ProgressBar, SelectInput, Table, UnorderedList

Components

@visulima/tui exposes Ink-compatible core components plus native renderer additions.

import { Box } from "@visulima/tui/components/box";
import { Cursor } from "@visulima/tui/components/cursor";
import { Newline } from "@visulima/tui/components/newline";
import { Spacer } from "@visulima/tui/components/spacer";
import { Static } from "@visulima/tui/components/static";
import { Text } from "@visulima/tui/components/text";
import { Transform } from "@visulima/tui/components/transform";
import { ConfirmInput } from "@visulima/tui/components/confirm-input";
import { FilePicker } from "@visulima/tui/components/file-picker";
import { Gradient } from "@visulima/tui/components/gradient";
import { Help } from "@visulima/tui/components/help";
import { MultiSelect } from "@visulima/tui/components/multi-select";
import { Paginator } from "@visulima/tui/components/paginator";
import { ProgressBar } from "@visulima/tui/components/progress-bar";
import { SelectInput } from "@visulima/tui/components/select-input";
import { Spinner } from "@visulima/tui/components/spinner";
import { Stopwatch } from "@visulima/tui/components/stopwatch";
import { Tab } from "@visulima/tui/components/tab";
import { Tabs } from "@visulima/tui/components/tabs";
import { TextInput } from "@visulima/tui/components/text-input";
import { Timer } from "@visulima/tui/components/timer";

Heavy components (subpath imports)

BigText, Code, DiffView, Markdown and Table each ship with at least one heavy optional peer dependency. They are exported from per-component subpaths so the optional deps are only loaded when you actually import the component.

import { BigText } from "@visulima/tui/components/big-text"; // requires: cfonts
import { Code } from "@visulima/tui/components/code"; // requires: shiki, @shikijs/langs, @shikijs/themes
import { DiffView } from "@visulima/tui/components/diff-view"; // requires: diff (+ shiki for syntax highlighting)
import { Markdown } from "@visulima/tui/components/markdown"; // requires: marked (+ shiki for code blocks)
import { Table } from "@visulima/tui/components/table"; // requires: @visulima/tabular

See the Package Map › Heavy component subpaths table for a full peer-dep matrix and npm install commands.

Box

Layout primitive backed by Yoga.

<Box
    flexDirection="row" // row | column | row-reverse | column-reverse
    flexGrow={1}
    flexShrink={1}
    width={40}
    height={10}
    minWidth={10}
    minHeight={4}
    padding={1}
    paddingX={2}
    margin={1}
    gap={1}
    alignItems="center"
    justifyContent="space-between"
    borderStyle="round" // single | double | round | bold | classic | singleDouble | doubleSingle
    borderColor="cyan"
    borderBackgroundColor="blue"
    borderTop={true}
    borderRight={true}
    borderBottom={true}
    borderLeft={true}
>
    {children}
</Box>

Overflow and CSS-Level Scrolling

Box supports CSS-level scrolling via overflow: 'scroll'. This is separate from the component-based ScrollView and provides native-style overflow clipping with integrated scrollbar rendering.

<Box
    overflow="scroll" // visible | hidden | scroll
    overflowX="hidden" // per-axis overflow control
    overflowY="scroll" // per-axis overflow control
    scrollTop={0} // vertical scroll position (rows)
    scrollLeft={0} // horizontal scroll position (columns)
    scrollbarThumbColor="gray" // scrollbar thumb color
    scrollbar={true} // show/hide scrollbar (default: true)
    height={10}
    width={40}
>
    {longContent}
</Box>

Sticky Headers

Elements within a scrollable Box can be pinned to the top or bottom of the viewport using the sticky prop. A sticky header remains pinned only while its parent section is visible.

<Box overflow="scroll" height={15}>
    <Box sticky>
        <Text bold>Section A</Text>
    </Box>
    {sectionAContent}

    <Box sticky>
        <Text bold>Section B</Text>
    </Box>
    {sectionBContent}
</Box>
PropTypeDescription
stickyboolean | 'top' | 'bottom'Pin this element during scroll. true and 'top' pin to the top; 'bottom' pins to the bottom.
opaquebooleanMarks this element as opaque for rendering optimization (not yet active).
scrollbarbooleanShow/hide the scrollbar for scrollable elements. Defaults to true.

Notes:

  • Box defaults to flexDirection="row" unless you set it explicitly.
  • Border side toggles (borderTop, etc.) are supported.
  • Per-side border colors are supported: borderTopColor, borderBottomColor, borderLeftColor, borderRightColor.
  • Per-side border background colors are supported: borderTopBackgroundColor, borderBottomBackgroundColor, borderLeftBackgroundColor, borderRightBackgroundColor.
  • Per-side dim is supported: borderTopDimColor, borderBottomDimColor, borderLeftDimColor, borderRightDimColor.

Text

Renders styled text.

<Text
    color="cyan" // named, #rgb, #rrggbb, rgb(r,g,b), or ANSI index 0-255
    backgroundColor="blue"
    bold
    italic
    underline
    strikethrough
    dim
    inverse
>
    Hello world
</Text>

dimColor is supported as an Ink-compat alias for dim.

Cursor

Declaratively position the terminal cursor. Two modes are available:

Inline mode (no props)

Place <Cursor /> after a <Text> node. The cursor appears where the preceding text ended, even when text wraps across lines.

<Box>
    <Text>{prompt + value}</Text>
    <Cursor />
</Box>

This is the recommended approach for text inputs. It handles text wrapping, wide characters (CJK, emoji), and multi-line output automatically because the cursor position is derived from the actual rendered text output.

For editable text with a cursor position in the middle:

<Box>
    <Text>{value.slice(0, cursorPos)}</Text>
    <Cursor />
    <Text>{value.slice(cursorPos)}</Text>
</Box>

Anchor mode

Pass anchorRef and/or x/y to position the cursor relative to another element's content origin.

const ref = useRef(null);

<Box ref={ref}>
    <Text>{content}</Text>
</Box>
<Cursor anchorRef={ref} x={5} y={1} />
PropTypeDefault
anchorRefRefObject<DOMElement/null>parent node
xnumber0
ynumber0

Notes:

  • <Cursor> must not be rendered inside <Text>.
  • If multiple <Cursor> components are rendered, the last one controls the terminal cursor.
  • If anchorRef is set but unresolved, the cursor is hidden for that frame.
  • See also useCursor() for imperative cursor control (e.g. IME support).

Code

Syntax-highlighted code display using Shiki. Supports 25+ languages with lazy loading and VS Code-quality tokenization.

Subpath import. Imported from @visulima/tui/components/code. Requires the optional peers shiki, @shikijs/langs, @shikijs/themes.

npm install shiki @shikijs/langs @shikijs/themes
import { Code } from "@visulima/tui/components/code";

<Code code='const x = "hello";' language="javascript" />
<Code code={source} language="python" showLineNumbers startLine={10} />
PropTypeDefaultDescription
codestring(required)Source code to highlight
languagestringProgramming language (e.g. "typescript")
themestring"github-dark-default"Shiki theme name
showLineNumbersbooleanfalseShow line number gutter
startLinenumber1Starting line number (for excerpts)
highlightLinesReadonlySet<number>Line numbers to visually emphasize

Supported languages: javascript, typescript, jsx, tsx, json, html, css, python, ruby, rust, go, java, bash, shell, sql, markdown, yaml, toml, xml, svelte, vue, and more. Unknown languages fall back to plain text.

The highlighter loads asynchronously — the first render shows plain text, then re-renders with colors once Shiki is ready.

DiffView

Display file differences with colored additions/deletions. Supports unified and split (side-by-side) views with optional inline character-level diff highlighting.

Subpath import. Imported from @visulima/tui/components/diff-view. Requires the optional peer diff. To enable syntax highlighting via the language prop, also install shiki, @shikijs/langs, and @shikijs/themes.

npm install diff
# optional, for syntax highlighting:
npm install shiki @shikijs/langs @shikijs/themes
import { DiffView } from "@visulima/tui/components/diff-view";

<DiffView oldText={original} newText={modified} />
<DiffView diff={unifiedDiffString} mode="split" />
<DiffView oldText={a} newText={b} oldLabel="v1.0" newLabel="v2.0" inlineDiff />
PropTypeDefaultDescription
oldTextstring""Original text (left / old)
newTextstring""Modified text (right / new)
diffstringPre-computed unified diff (overrides oldText/newText)
mode"unified" | "split""unified"Display mode
showLineNumbersbooleantrueShow line numbers
contextnumber3Unchanged context lines around changes
inlineDiffbooleantrueCharacter-level highlighting within changed lines
oldLabelstring"old"Label for the old version
newLabelstring"new"Label for the new version
languagestringProgramming language for syntax highlighting
themestring"github-dark-default"Shiki theme for syntax highlighting

Syntax highlighting in diffs

When language is provided, diff content is highlighted with Shiki (same engine as the Code component). Syntax colors are preserved for context lines, and overlaid with diff colors (red/green) for changed lines.

<DiffView oldText={oldCode} newText={newCode} language="typescript" />

Colors: additions in green (+), deletions in red (-), context lines dimmed. Inline diff highlights changed characters with inverted background.

FilePicker

A file system browser for picking files and navigating directories in the terminal.

import { FilePicker } from "@visulima/tui/components/file-picker";
<FilePicker
    onSelect={(entry) => console.log("Selected:", entry.path)}
    filter={{ extensions: [".ts", ".tsx"] }}
/>
<FilePicker
    initialPath="/home/user/projects"
    showSize
    showPermissions
    limit={15}
    onCancel={() => exit()}
/>
PropTypeDefaultDescription
initialPathstringprocess.cwd()Starting directory
filterFilePickerFilter{}File filtering options (see below)
onSelect(entry: FileEntry) => voidCalled when a file is selected (Enter)
onCancel() => voidCalled when Escape is pressed
showPermissionsbooleanfalseShow file permissions column
showSizebooleanfalseShow file size column
isFocusedbooleantrueWhether keyboard navigation is active
limitnumber10Visible items (viewport height)
accentColorstring"cyan"Focused item highlight color
directoryColorstring"blue"Directory name color
fileColorstring"white"File name color

Filter options

FieldTypeDefaultDescription
extensionsstring[]Allowed extensions (e.g. [".ts", ".json"])
showHiddenbooleanfalseInclude hidden files (dotfiles)
showDirectoriesbooleantrueInclude directories
showFilesbooleantrueInclude regular files

FileEntry shape

type FileEntry = {
    name: string;
    path: string;
    isDirectory: boolean;
    isHidden: boolean;
    size: number;
    permissions: string;
};

Keyboard

KeyAction
/ Move focus
EnterOpen directory or select file
Backspace / Go to parent directory
EscapeCancel (onCancel)
Page Up/DownJump by viewport height
Home / EndJump to first/last entry
.Refresh directory listing

BigText

Render large ASCII art text using CFonts. Ported from ink-big-text.

Subpath import. Imported from @visulima/tui/components/big-text. Requires the optional peer cfonts.

npm install cfonts
import { BigText } from "@visulima/tui/components/big-text";

<BigText text="Hello" />
<BigText text="Cool" font="chrome" colors={["red", "blue"]} />
PropTypeDefault
textstringrequired
fontFont'block'
align'left' | 'center' | 'right''left'
colorsstring[]['system']
backgroundColorBackgroundColor'transparent'
letterSpacingnumber1
lineHeightnumber1
spacebooleantrue
maxLengthnumber0

Available fonts: block, slick, tiny, grid, pallet, shade, simple, simpleBlock, 3d, simple3d, chrome, huge.

Gradient

Apply a terminal color gradient to child text. Ported from ink-gradient, uses @visulima/colorize internally.

import { Gradient } from "@visulima/tui/components/gradient";
import { Text } from "@visulima/tui/components/text";
<Gradient name="rainbow">
    <Text>Hello, World!</Text>
</Gradient>

<Gradient colors={["#ff0000", "#00ff00", "#0000ff"]}>
    <Text>Custom gradient</Text>
</Gradient>
PropTypeDefault
nameGradientName
colorsGradientColors

name and colors are mutually exclusive — exactly one must be provided.

Built-in presets: rainbow, cristal, teen, mind, morning, vice, passion, fruit, instagram, atlas, retro, summer, pastel.

Help

Auto-generated keybinding help bar. Renders key-description pairs from an array of KeyBinding definitions (typically returned by useKeyBindings).

import { Help } from "@visulima/tui/components/help";
import { useKeyBindings } from "@visulima/tui/hooks/use-key-bindings";
const { bindings } = useKeyBindings([
    { binding: { key: "q", description: "Quit" }, handler: () => exit() },
    { binding: { key: ["↑", "k"], description: "Up", group: "Navigation" }, handler: () => moveUp() },
]);

<Help bindings={bindings} />
<Help bindings={bindings} mode="full" />
PropTypeDefaultDescription
bindingsKeyBinding[](required)Key bindings to display
mode"short" | "full""short"Short = single line; full = multi-column grid
separatorstring" · "Separator between pairs in short mode
keyColorstring"cyan"Color for key labels
descriptionColorstringColor for descriptions (defaults to dim)
maxColumnsnumber3Max columns in full mode

Short mode

Renders all bindings on a single line, truncated to terminal width:

q Quit · ↑/k Up · ↓/j Down · enter Select

Full mode

Groups bindings by the group field into columns:

Navigation          Actions
↑/k Up              enter Select
↓/j Down            q Quit

Key names are automatically humanized: "upArrow""↑", "return""enter", "escape""esc", etc.

Create clickable hyperlinks in the terminal using OSC 8 sequences. Ported from ink-link.

import { Link } from "@visulima/tui/components/link";
import { Text } from "@visulima/tui/components/text";
<Link url="https://example.com">
    <Text color="cyan">My Website</Text>
</Link>

// Custom fallback for unsupported terminals
<Link url="https://example.com" fallback={(text, url) => `[${text}](${url})`}>
    <Text>Docs</Text>
</Link>
PropTypeDefaultDescription
urlstringThe URL to link to (required)
fallbackboolean | ((text: string, url: string) => string)trueFallback for unsupported terminals
childrenReactNodeLink text content

When fallback is true, unsupported terminals show: My Website https://example.com. When false, only the text is shown. A function receives (text, url) for custom formatting.

Supported terminals.

Markdown

Render Markdown content as terminal UI elements. Parses with marked and maps tokens to Ink components. Code blocks are syntax-highlighted via the Code component.

Subpath import. Imported from @visulima/tui/components/markdown. Requires the optional peer marked. Code blocks pull in shiki, @shikijs/langs, @shikijs/themes; tables pull in @visulima/tabular — install whichever your content needs.

npm install marked
# for code-block highlighting:
npm install shiki @shikijs/langs @shikijs/themes
# for table rendering inside markdown:
npm install @visulima/tabular
import { Markdown } from "@visulima/tui/components/markdown";

<Markdown>{"# Hello World\n\nThis is **bold** and *italic*."}</Markdown>
<Markdown codeTheme="github-dark-default">{"```js\nconst x = 1;\n```"}</Markdown>
PropTypeDefaultDescription
childrenstring(required)Markdown source string
codeThemestring"github-dark-default"Shiki theme for code blocks
maxWidthnumberterminal widthMaximum text wrap width
streamingbooleanfalseProgressive rendering for LLM output

Streaming mode

Enable streaming when rendering Markdown that arrives incrementally (e.g., token-by-token from an AI model). The component handles incomplete Markdown gracefully — unclosed code fences and partial blocks render as text until they close.

const [text, setText] = useState("");
// ... text grows as tokens arrive ...
<Markdown streaming>{text}</Markdown>;

Supported Markdown features

  • Headings (H1-H6) — bold, color-coded by depth
  • Paragraphs — with word wrapping
  • Code blocks — syntax highlighted via \<Code> component
  • Inline code — rendered with inverse styling
  • Bold, italic, strikethrough
  • Links — rendered via the existing \<Link> component (OSC 8)
  • Ordered and unordered lists — via existing \<OrderedList> / \<UnorderedList>
  • Blockquotes — with left border
  • Horizontal rules
  • Tables — via existing \<Table> component
  • Images — displayed as [image: alt text] (terminals cannot render images)

Newline

<Newline />
<Newline count={2} />

Spacer

<Box flexDirection="row">
    <Text>Left</Text>
    <Spacer />
    <Text>Right</Text>
</Box>

Static

Append-only region for completed items and log lines.

<Static items={completedTasks}>
    {(task) => (
        <Box key={task.id}>
            <Text color="green">✓ {task.name}</Text>
        </Box>
    )}
</Static>

Static only appends new tail items from items.

Transform

Applies a transform function to the collected text output of its subtree.

<Transform transform={(s) => s.toUpperCase()}>
    <Text>hello world</Text>
</Transform>

Spinner

Animated spinner. Backed by @visulima/spinner (replaces the previous cli-spinners dependency). The frame set and tick interval are determined by the named preset; pass type to switch presets.

<Spinner />
<Spinner type="dots" />
<Spinner type="wave" />
PropTypeDefaultDescription
typeSpinnerName"dots"Any preset name from @visulima/spinner

Available presets include dots, breathe, cascade, checkerboard, columns, diagSwipe, dna, doubleArrow, fillSweep, helix, infinity, orbit, pulse, rain, scan, scanline, snake, sparkle, wave, waveRows, plus every preset re-exported from cli-spinners. See packages/terminal/spinner/src/spinners.ts for the full list.

Stopwatch

A count-up elapsed time display. Exposes start, stop, toggle, reset, and lap controls via ref.

import { Stopwatch } from "@visulima/tui/components/stopwatch";
import { useRef } from "react";

const ref = useRef(null);

<Stopwatch ref={ref} autoStart />
<Stopwatch autoStart color="green" bold />
<Stopwatch format={(ms) => `${(ms / 1000).toFixed(1)}s`} />
PropTypeDefaultDescription
autoStartbooleanfalseStart counting immediately
intervalnumber1000Tick interval in milliseconds
format(elapsed: number) => stringMM:SS/HH:MM:SSCustom time formatter
colorstringText color
boldbooleanBold text

Ref methods

ref.current.start(); // Start counting
ref.current.stop(); // Pause
ref.current.toggle(); // Toggle start/stop
ref.current.reset(); // Reset to 00:00 and stop
ref.current.lap(); // Capture current elapsed time

For headless usage, use the useStopwatch hook directly.

Timer

A countdown timer display. Exposes start, stop, toggle, and reset controls via ref.

import { Timer } from "@visulima/tui/components/timer";
import { useRef } from "react";

const ref = useRef(null);

<Timer ref={ref} duration={60_000} autoStart />
<Timer duration={300_000} autoStart color="red" onTimeout={() => alert("Done!")} />
PropTypeDefaultDescription
durationnumber(required)Countdown duration in milliseconds
autoStartbooleanfalseStart counting immediately
intervalnumber1000Tick interval in milliseconds
onTimeout() => voidCalled when the timer reaches zero
format(remaining: number) => stringMM:SS/HH:MM:SSCustom time formatter
colorstringText color
boldbooleanBold text

Ref methods

ref.current.start(); // Start countdown
ref.current.stop(); // Pause
ref.current.toggle(); // Toggle start/stop
ref.current.reset(); // Reset to original duration and stop

For headless usage, use the useTimer hook directly.

ProgressBar

A terminal progress bar that fills proportionally to a percentage value. Ported from ink-progress-bar.

import { ProgressBar } from "@visulima/tui/components/progress-bar";
<ProgressBar percent={0.5} />
<ProgressBar percent={0.75} color="green" left={10} right={5} />
<ProgressBar percent={1} character="▓" rightPad />

All Text props (color, bold, dimColor, etc.) are forwarded to the underlying \<Text>.

PropTypeDefaultDescription
percentnumber1Completion between 0 and 1 (clamped)
columnsnumber0Override terminal width (0 = auto-detect)
leftnumber0Columns reserved on the left (e.g. for labels)
rightnumber0Columns reserved on the right
characterstring'█'Fill character for the completed portion
rightPadbooleanfalsePad remaining space with whitespace

Example with labels:

<Box>
    <Text>Progress: </Text>
    <ProgressBar percent={0.6} left={10} right={5} color="cyan" />
    <Text> 60%</Text>
</Box>

Slider

A keyboard-driven range input for selecting numeric values. Supports horizontal and vertical orientations.

import { Slider } from "@visulima/tui/components/slider";
<Slider defaultValue={50} onChange={(v) => console.log(v)} />
<Slider defaultValue={22} min={10} max={40} step={1} accentColor="red" width={30} />
<Slider defaultValue={60} orientation="vertical" width={8} />
PropTypeDefaultDescription
valuenumberControlled value
defaultValuenumber0Initial value (uncontrolled)
minnumber0Minimum value
maxnumber100Maximum value
stepnumber1Increment per arrow key press
orientation"horizontal" | "vertical""horizontal"Layout direction
widthnumber20Track width in columns (or rows if vertical)
isFocusedbooleantrueWhether the component responds to input
isDisabledbooleanfalseIgnores all input and dims the slider
filledCharacterstring"█"Character for the filled portion
emptyCharacterstring"░"Character for the empty portion
thumbCharacterstring"●"Character marking the current position
accentColorstring"green"Color for filled portion and thumb
defaultColorstring"gray"Color for empty portion
onChange(value: number) => voidCalled when value changes

Keyboard

  • Left/Right arrows (horizontal) or Up/Down (vertical): move by step
  • Home/End: jump to min/max
  • Page Up/Down: move by 10 × step
  • 0–9: jump to 0%, 10%, … 90% of the range

Controlled mode

const [value, setValue] = useState(50);
<Slider value={value} onChange={setValue} />;

ConsoleOverlay

A dockable panel that captures console.log/warn/error/debug output and displays it within the TUI. Similar to browser devtools console.

import { Box } from "@visulima/tui/components/box";
import { ConsoleOverlay } from "@visulima/tui/components/console-overlay";
<Box flexDirection="column" height="100%">
    <Box flexGrow={1}>{/* main app content */}</Box>
    <ConsoleOverlay dock="bottom" height={6} />
</Box>;
PropTypeDefaultDescription
dock"top" | "bottom""bottom"Panel position
heightnumber8Visible rows
maxEntriesnumber200Buffer size
showTimestampbooleantrueShow [HH:MM:SS] prefix
showLevelbooleantrueShow level label (LOG/WRN/ERR)
filterConsoleLevel[]all levelsWhich levels to capture

Entries are color-coded: gray (debug), white (log), blue (info), yellow (warn), red (error). Auto-scrolls to show the latest entry.

ConfirmInput

A Y/N confirmation prompt. Inspired by ink-ui.

ConfirmInput demo
import { ConfirmInput } from "@visulima/tui/components/confirm-input";
import { Text } from "@visulima/tui/components/text";
<Text>Do you want to continue? </Text>
<ConfirmInput
    onConfirm={() => console.log("Confirmed!")}
    onCancel={() => console.log("Cancelled.")}
/>
PropTypeDefaultDescription
defaultChoice"confirm" | "cancel""confirm"Default action when Enter is pressed
isDisabledbooleanfalseIgnores all input when true
submitOnEnterbooleantrueWhether Enter submits the default choice
onConfirm() => void(required)Called when user presses Y
onCancel() => void(required)Called when user presses N

Shows Y/n when default is confirm, y/N when default is cancel. Press Y/y to confirm, N/n to cancel, Enter to submit the default.

MultiSelect

Multi-choice selection component with checkboxes and keyboard navigation. Inspired by ink-ui.

MultiSelect demo
import { MultiSelect } from "@visulima/tui/components/multi-select";
const options = [
    { label: "TypeScript", value: "ts" },
    { label: "JavaScript", value: "js" },
    { label: "Python", value: "py" },
];

<MultiSelect options={options} onSubmit={(values) => console.log("Selected:", values)} />;
PropTypeDefaultDescription
optionsMultiSelectOption[](required)Available choices
defaultValuestring[][]Initially selected values
isDisabledbooleanfalseIgnores all input when true
isFocusedbooleantrueWhether the component captures input
limitnumberMax visible options; auto-limits to terminal height
accentColorstring"blue"Color for the focused indicator
defaultColorstringColor for unfocused, unselected labels
onChange(values: string[]) => voidCalled when selection changes (space)
onSubmit(values: string[]) => voidCalled when Enter is pressed

Keyboard: arrow keys or j/k to navigate, Space to toggle, a to select/deselect all, Enter to submit.

TextInput

Single-line text input with cursor navigation, autocomplete suggestions, and optional password masking. Inspired by ink-ui.

TextInput demo
import { TextInput } from "@visulima/tui/components/text-input";
<TextInput placeholder="Enter your name..." onSubmit={(value) => console.log("Name:", value)} />;
PropTypeDefaultDescription
defaultValuestring""Starting value
isDisabledbooleanfalseIgnores all input when true
maskbooleanfalseRenders asterisks instead of characters
placeholderstringGreyed-out text when input is empty
suggestionsstring[]Autocomplete candidates (case-sensitive prefix match)
onChange(value: string) => voidCalled on every keystroke
onSubmit(value: string) => voidCalled when Enter is pressed

Keyboard: left/right arrows for cursor, Home/End or Ctrl+A/E for start/end, Ctrl+U/K/W for line editing, Backspace/Delete, right arrow to accept suggestion, Enter to submit (accepts suggestion if pending).

Password mode

<TextInput mask onSubmit={(password) => authenticate(password)} />

Autocomplete

<TextInput suggestions={["react", "react-dom", "react-native"]} onSubmit={install} />

The first matching suggestion is shown dimmed after the cursor. Press right arrow or Enter to accept it.

Textarea

Multi-line text input with cursor navigation, selection, clipboard copy (OSC 52), undo/redo, paste support, and viewport scrolling.

import { Textarea } from "@visulima/tui/components/textarea";
<Textarea
    placeholder="Write something..."
    rows={8}
    showLineNumbers
    onChange={(value) => console.log(value)}
    onSubmit={(value) => console.log("Submitted:", value)}
/>;
PropTypeDefaultDescription
defaultValuestring""Starting content
placeholderstringGreyed-out text shown when empty
rowsnumber5Visible rows (viewport height)
maxRowsnumberAuto-grow up to this height before scrolling
isDisabledbooleanfalseIgnores all input and dims the text
isFocusedbooleantrueWhether the component responds to input
showLineNumbersbooleanfalseShow line numbers in the left gutter
tabSizenumber2Number of spaces inserted for Tab
onChange(value: string) => voidCalled on every edit
onSubmit(value: string) => voidCalled on Meta+Enter or Ctrl+Enter

Keyboard

KeyAction
Arrow keysMove cursor
Shift+ArrowsExtend selection
Home / Ctrl+AStart of line
End / Ctrl+EEnd of line
Ctrl+HomeStart of buffer
Ctrl+EndEnd of buffer
EnterInsert newline
Meta+Enter / Ctrl+EnterSubmit (onSubmit)
Backspace / DeleteDelete character
Ctrl+UKill to line start
Ctrl+KKill to line end
Ctrl+WKill word backward
TabInsert spaces (tabSize)
Ctrl+ZUndo
Ctrl+YRedo
Ctrl+Shift+ASelect all
Ctrl+C (with selection)Copy to clipboard (OSC 52)
EscapeClear selection
Paste (bracketed)Insert pasted text

Auto-growing height

<Textarea rows={3} maxRows={15} placeholder="Grows from 3 to 15 rows..." />

With line numbers

<Textarea showLineNumbers defaultValue={"function hello() {\n  return 'world';\n}"} rows={5} />

SelectInput

Interactive select input for choosing from a list of options. Ported from ink-select-input.

SelectInput demo
import { SelectInput } from "@visulima/tui/components/select-input";
const items = [
    { label: "TypeScript", value: "ts" },
    { label: "JavaScript", value: "js" },
    { label: "Python", value: "py" },
];

<SelectInput items={items} onSelect={(item) => console.log(item.value)} />;

Navigate with arrow keys or j/k, select with Enter, or jump to an item by pressing a number key (1–9).

Props

PropTypeDefaultDescription
itemsItem\<V>[][]Choices to display
isFocusedbooleantrueWhether the component captures keyboard input
initialIndexnumberInitially-selected item index; omit for no initial selection
indexnumberControlled selected index (see below)
limitnumberMax visible items; enables scroll windowing
accentColorstring"blue"Color for the selected item and indicator
defaultColorstringColor for unselected items
indicatorComponentFC\<IndicatorProps>SelectInputIndicatorCustom indicator component
itemComponentFC\<ItemProps>SelectInputItemCustom item component
onSelect(item: Item\<V>) => voidCalled on Enter or number key press
onHighlight(item: Item\<V>, index: number) => voidCalled when selection moves; second arg is the new index
resetOnItemsChangebooleantrueReset selection when item values change

Each item in the items array must have a label (display text) and a value (returned on select). An optional key provides a stable React key. An optional action function is called automatically when the item is selected.

Item actions

Instead of switching on value in the onSelect handler, you can define per-item behavior inline:

const items = [
    { label: "Build", value: "build", action: () => runBuild() },
    { label: "Test", value: "test", action: () => runTests() },
    { label: "Deploy", value: "deploy", action: () => deploy() },
];

<SelectInput items={items} initialIndex={0} />;

When an item is selected (Enter or number key), onSelect fires first (if provided), then item.action() is called. Both are optional — you can use either or both.

Separators

Add non-focusable separator lines between groups of items. Separators are skipped during keyboard navigation and cannot be selected:

const items = [
    { label: "Cut", value: "cut" },
    { label: "Copy", value: "copy" },
    { label: "Paste", value: "paste" },
    { isSeparator: true },
    { label: "Select All", value: "selectAll" },
];

<SelectInput items={items} initialIndex={0} />;

A separator renders as ─── by default. Pass a custom label to change the text:

{ isSeparator: true, label: "── Advanced ──" }

The SeparatorItem type is exported for use in typed item arrays:

import type { SelectInputEntry, SelectInputSeparator } from "@visulima/tui/components/select-input";
const items: SelectInputEntry<string>[] = [{ label: "Option A", value: "a" }, { isSeparator: true }, { label: "Option B", value: "b" }];

Note: the reset check compares item values only — changing labels or other properties while keeping the same values will not trigger a reset. Set resetOnItemsChange={false} to preserve the selection across all item changes.

No initial selection

When initialIndex is omitted, no item is highlighted until the user presses a navigation key. This is useful for rendering a list without a pre-selected value, similar to an empty radio group:

<SelectInput items={items} onHighlight={(item) => console.log("Browsing:", item.label)} />

The first down arrow or j press highlights the first item; the first up arrow or k press highlights the last. Enter is a no-op until an item is highlighted. Number keys (1–9) still work for direct selection.

To start with the first item already highlighted, pass initialIndex={0}.

Controlled mode

Pass index to control the selection externally. When index is set, initialIndex is ignored. Update index from your onHighlight callback to track the selection:

const [selectedIndex, setSelectedIndex] = useState(0);

<SelectInput
    items={items}
    index={selectedIndex}
    onHighlight={(item, idx) => setSelectedIndex(idx)}
    onSelect={(item) => console.log("Selected:", item.value)}
/>;

Scroll windowing

When no limit is specified and the list has more items than the terminal has rows, the component automatically caps the visible items to the terminal height. This prevents overflow without any manual measurement.

You can also set limit explicitly to control the window size:

<SelectInput items={longList} limit={5} />

The list wraps around when navigating past the first or last visible item.

Custom colors

Override the default blue highlight with accentColor and defaultColor:

<SelectInput items={items} accentColor="green" defaultColor="gray" />

These colors are passed through to the indicator and item components.

Focus dimming

When multiple SelectInput components are on screen, the unfocused ones automatically dim their selected item using dimColor. This makes it easy to tell which list has focus:

<Box>
    <SelectInput items={leftItems} initialIndex={0} isFocused={leftActive} />
    <SelectInput items={rightItems} initialIndex={0} isFocused={!leftActive} />
</Box>

The isFocused prop is passed through to both indicatorComponent and itemComponent, so custom components can implement their own unfocused styling.

Custom components

Replace the default indicator or item rendering:

import { Box } from "@visulima/tui/components/box";
import { Text } from "@visulima/tui/components/text";
const CustomIndicator = ({ isSelected, isFocused, accentColor }) => (
    <Box marginRight={1}>
        <Text color={isSelected ? accentColor : undefined} dimColor={!isFocused}>
            {isSelected ? "→" : " "}
        </Text>
    </Box>
);

const CustomItem = ({ label, isSelected, isFocused, accentColor, defaultColor }) => (
    <Text bold={isSelected} color={isSelected ? accentColor : defaultColor} dimColor={isSelected && !isFocused}>
        {label}
    </Text>
);

<SelectInput items={items} indicatorComponent={CustomIndicator} itemComponent={CustomItem} />;

Custom components receive isSelected, isFocused, accentColor, and (for items) defaultColor plus the item's own label/value/key fields.

Tab / Tabs

Tabbed navigation component with keyboard support. Inspired by ink-tab.

import { Tab } from "@visulima/tui/components/tab";
import { Tabs } from "@visulima/tui/components/tabs";
<Tabs onChange={(name, tab) => console.log("Active:", name)}>
    <Tab name="files">Files</Tab>
    <Tab name="search">Search</Tab>
    <Tab name="settings">Settings</Tab>
</Tabs>;

Tabs Props

PropTypeDefaultDescription
childrenReactNode(required)<Tab> elements
onChange(name: string, tab: ReactElement) => void(required)Fired when the active tab changes (both modes). In controlled mode the parent must update value from this callback.
valuestringControlled active tab name. When set, the component is controlled and defaultValue is ignored.
defaultValuestringInitial active tab in uncontrolled mode
flexDirection"row" | "column" | "row-reverse" | "column-reverse""row"Layout direction
isFocusedboolean | nullundefinedFocus state; false disables input, null = unmanaged
showIndexbooleantrueShow 1-based index numbers before tab names
widthnumber | stringContainer width (also separator width in column layouts)
keyMapKeyMapCustom keyboard bindings
colorsTabColorsActive tab color customization

Controlled mode

const [active, setActive] = useState("files");

<Tabs value={active} onChange={(name) => setActive(name)}>
    <Tab name="files">Files</Tab>
    <Tab name="search">Search</Tab>
</Tabs>;

Tab Props

PropTypeDefaultDescription
namestring(required)Unique identifier for this tab
childrenReactNode(required)Content rendered inside the tab

Keyboard

  • Left/Right arrows (or Up/Down in column layout): navigate between tabs
  • Tab/Shift+Tab: cycle through tabs (only when isFocused is unmanaged)
  • Meta/Cmd + 1–9: jump to a specific tab by index

Column layout

<Tabs flexDirection="column" onChange={handleChange}>
    <Tab name="a">Alpha</Tab>
    <Tab name="b">Beta</Tab>
</Tabs>

Custom colors

<Tabs colors={{ activeTab: { color: "cyan", backgroundColor: "white" } }} onChange={handleChange}>
    <Tab name="one">One</Tab>
    <Tab name="two">Two</Tab>
</Tabs>

Table

Render data as a formatted table in the terminal. Uses @visulima/tabular as the rendering engine, supporting multiple border styles, column configuration, cell formatting, and word wrap. Ported from ink-table.

Subpath import. Imported from @visulima/tui/components/table. Requires the optional peer @visulima/tabular.

npm install @visulima/tabular
import { Table } from "@visulima/tui/components/table";

const data = [
    { name: "Alice", age: 30, city: "NYC" },
    { name: "Bob", age: 25, city: "LA" },
];

<Table data={data} />
<Table data={data} borderStyle="rounded" padding={2} />

Props

PropTypeDefaultDescription
dataT[](required)Array of objects to display as rows
columns(string | ColumnConfig)[]all keysColumns to display and their order
paddingnumber1Cell padding (left and right) in characters
borderStylestring | BorderStyle"default"Border preset or custom object (see below)
maxWidthnumberMaximum table width; columns truncate/wrap to fit
showHeaderbooleantrueWhether to show the header row
formatCell(value, column, rowIndex) => stringCustom cell value formatter
formatHeader(column) => stringCustom header label formatter
skeletonstring""Replacement for null/undefined cells
wordWrapbooleanfalseEnable word wrap instead of truncation

Column Configuration

Columns accept either simple key strings or ColumnConfig objects for advanced control:

<Table data={data} columns={[{ key: "name", header: "Full Name", width: 20, align: "left" }, { key: "age", header: "Years", align: "right" }, "city"]} />
ColumnConfig fieldTypeDescription
keystring (required)Key from the data object
headerstringCustom header label (defaults to key)
widthnumberFixed column width in characters
align"left" | "center" | "right"Horizontal alignment

Border Styles

Available presets: "default", "rounded", "double", "minimal", "dots", "markdown", "ascii", "none", "block", "thick".

<Table data={data} borderStyle="rounded" />
<Table data={data} borderStyle="ascii" />
<Table data={data} borderStyle="none" />

You can also pass a custom BorderStyle object from @visulima/tabular for full control.

Custom Formatting

<Table
    data={data}
    formatCell={(value, column, rowIndex) => {
        if (column === "name") return String(value).toUpperCase();
        return String(value ?? "");
    }}
    formatHeader={(column) => column.toUpperCase()}
    skeleton="N/A"
/>

Note: Unlike the original ink-table, this component uses function-based formatters (formatCell, formatHeader) instead of custom React component props (header, cell, skeleton), because the table is rendered as a single string via @visulima/tabular.

Badge

Uppercase colored label badge for status indicators. Inspired by ink-ui.

Badge demo
import { Badge } from "@visulima/tui/components/badge";
<Badge>new</Badge>
<Badge color="red">error</Badge>
<Badge color="green">success</Badge>
PropTypeDefaultDescription
childrenReactNode(required)Label content; strings are auto-uppercased
colorstring"magenta"Background color for the badge

StatusMessage

Status notification with a colored variant icon. Inspired by ink-ui.

StatusMessage demo
import { StatusMessage } from "@visulima/tui/components/status-message";
<StatusMessage variant="success">Build completed</StatusMessage>
<StatusMessage variant="error">Connection failed</StatusMessage>
<StatusMessage variant="warning">Disk space low</StatusMessage>
<StatusMessage variant="info">3 updates available</StatusMessage>
PropTypeDefaultDescription
childrenReactNode(required)Message text
variant"success" | "error" | "warning" | "info"(required)Determines icon and color

Icons: ✔ (success/green), ✖ (error/red), ⚠ (warning/yellow), ℹ (info/blue).

Alert

Bordered alert box with a variant-specific icon and optional title. Inspired by ink-ui.

Alert demo
import { Alert } from "@visulima/tui/components/alert";
<Alert variant="info">Check the docs for details.</Alert>
<Alert variant="warning" title="Deprecation Notice">
    This API will be removed in v3.
</Alert>
PropTypeDefaultDescription
childrenReactNode(required)Alert message
variant"info" | "success" | "error" | "warning"(required)Border color and icon
titlestringBold heading above message

UnorderedList

Bullet list with nesting support and customizable markers. Inspired by ink-ui.

UnorderedList demo
import { UnorderedList } from "@visulima/tui/components/unordered-list";
<UnorderedList items={[{ label: "TypeScript" }, { label: "JavaScript" }, { label: "Python", children: [{ label: "Django" }, { label: "Flask" }] }]} />;
PropTypeDefaultDescription
itemsUnorderedListEntry[](required)List entries with label and optional children
markerstring | string[]["─", "◦", "▪"]Bullet symbol(s); arrays cycle by depth

Each entry has label: ReactNode and optional children: UnorderedListEntry[] for nesting.

OrderedList

Numbered list with hierarchical nesting and auto-padded markers. Inspired by ink-ui.

OrderedList demo
import { OrderedList } from "@visulima/tui/components/ordered-list";
<OrderedList
    items={[
        { label: "Install dependencies" },
        { label: "Configure", children: [{ label: "Create config file" }, { label: "Set environment variables" }] },
        { label: "Deploy" },
    ]}
/>;
PropTypeDefaultDescription
itemsOrderedListEntry[](required)List entries with label and optional children

Numbers are right-padded for alignment (e.g., 1., 2., 10.). Nested lists produce hierarchical markers (1., 1.1., 1.2.).

Paginator

A pagination component with a render-prop pattern and configurable page indicators. Handles keyboard navigation and slices items by page.

import { Box } from "@visulima/tui/components/box";
import { Paginator } from "@visulima/tui/components/paginator";
import { Text } from "@visulima/tui/components/text";
<Paginator items={allItems} pageSize={5}>
    {(pageItems, meta) => (
        <Box flexDirection="column">
            {pageItems.map((item, i) => (
                <Text key={i}>{item}</Text>
            ))}
        </Box>
    )}
</Paginator>;
PropTypeDefaultDescription
itemsT[](required)Full item list to paginate
pageSizenumber(required)Items per page
children(pageItems: T[], meta: PageMeta) => ReactNode(required)Render prop for page content
pagenumberControlled current page (0-indexed)
defaultPagenumber0Initial page for uncontrolled mode
onChange(page: number) => voidCalled when page changes
isFocusedbooleantrueWhether keyboard navigation is active
style"dots" | "numeric" | "fraction""dots"Indicator style
indicatorColorstring"cyan"Color for the active indicator

PageMeta

The render-prop receives a PageMeta object as the second argument:

type PageMeta = {
    currentPage: number;
    totalPages: number;
    startIndex: number;
    endIndex: number;
    isFirstPage: boolean;
    isLastPage: boolean;
};

Indicator styles

  • dots: ● ○ ○ ○ — active dot highlighted
  • numeric: ‹ 1 / 4 › — arrows dim at boundaries
  • fraction: 1/4 — compact display

Keyboard

KeyAction
/ Page UpPrevious page
/ Page DownNext page
HomeFirst page
EndLast page

Controlled mode

const [page, setPage] = useState(0);
<Paginator items={data} pageSize={10} page={page} onChange={setPage}>
    {(items) => <MyList items={items} />}
</Paginator>;

TreeView

Interactive tree view with keyboard navigation, expand/collapse, single/multi selection, async lazy-loading, and viewport virtualization. Ported from ink-tree-view.

import { TreeView } from "@visulima/tui/components/tree-view";
import type { TreeNode } from "@visulima/tui/components/tree-view";
const data: TreeNode[] = [
    {
        id: "src",
        label: "src",
        children: [
            { id: "src/index.ts", label: "index.ts" },
            {
                id: "src/components",
                label: "components",
                children: [{ id: "src/components/App.tsx", label: "App.tsx" }],
            },
        ],
    },
    { id: "package.json", label: "package.json" },
];

<TreeView data={data} defaultExpanded="all" />;

Keyboard controls

KeyAction
/ Move focus
Expand node, or move to first child if already expanded
Collapse node, or move to parent if already collapsed
EnterToggle expand (none mode) or select (single/multi mode)
SpaceToggle selection (multi mode) or expand (none/single)
HomeFocus first node
EndFocus last node

Props

PropTypeDefaultDescription
dataTreeNode\<T>[](required)Tree data. Must be referentially stable (use useMemo).
selectionMode"none" | "single" | "multiple""none""single" shows a tick, "multiple" shows checkboxes.
defaultExpandedReadonlySet\<string> | "all"IDs of nodes expanded on mount. Pass "all" to expand everything.
defaultSelectedReadonlySet\<string>IDs of nodes selected on mount.
visibleNodeCountnumberInfinityLimits visible rows (viewport virtualization). Scroll indicators appear automatically.
renderNode(props: TreeNodeRendererProps\<T>) => ReactNodeCustom node renderer. Receives { node, state }.
loadChildren(node: TreeNode\<T>) => Promise\<TreeNode\<T>[]>Async function called when expanding a node with isParent: true and no children.
onFocusChange(nodeId: string) => voidCalled when the focused node changes.
onExpandChange(expandedIds: ReadonlySet\<string>) => voidCalled when the expanded set changes.
onSelectChange(selectedIds: ReadonlySet\<string>) => voidCalled when the selection changes.
onLoadError(nodeId: string, error: Error) => voidCalled when loadChildren rejects.
isDisabledbooleanfalseIgnores keyboard input when true.
ariaLabelstring"Tree view"Accessible label for the tree container.

TreeNode\<T> shape

type TreeNode<T = Record<string, unknown>> = {
    id: string; // unique across the whole tree
    label: string; // display text
    data?: T; // arbitrary user data
    children?: TreeNode<T>[]; // child nodes
    isParent?: boolean; // show expand indicator even with no children (for async loading)
};

Custom rendering

Use renderNode for full control over each row:

<TreeView
    data={data}
    defaultExpanded="all"
    renderNode={({ node, state }) => (
        <Box gap={1}>
            <Text dimColor>{"  ".repeat(state.depth)}</Text>
            <Text color={state.isFocused ? "blue" : undefined}>
                {state.hasChildren ? (state.isExpanded ? "📂" : "📁") : "📄"} {node.label}
            </Text>
        </Box>
    )}
/>

Async lazy-loading

Mark nodes with isParent: true and provide a loadChildren callback:

const data: TreeNode[] = [{ id: "root", label: "Remote Files", isParent: true }];

<TreeView
    data={data}
    loadChildren={async (node) => {
        const response = await fetch(`/api/files/${node.id}`);
        return response.json();
    }}
    onLoadError={(nodeId, error) => console.error(`Failed to load ${nodeId}:`, error)}
/>;

A loading indicator () is shown while children are being fetched.

Viewport virtualization

Limit the visible rows for large trees:

<TreeView data={largeTree} defaultExpanded="all" visibleNodeCount={15} />

Scroll indicators (↑ N more above / ↓ N more below) appear automatically.

Headless usage

For full control, use the hooks directly instead of the \<TreeView> component:

import { useTreeView, useTreeViewState } from "@visulima/tui/components/tree-view";
function CustomTree({ data }) {
    const state = useTreeViewState({ data, selectionMode: "single" });
    useTreeView({ state, selectionMode: "single" });

    return (
        <Box flexDirection="column">
            {state.viewportNodes.map(({ node, state: nodeState }) => (
                <Text key={node.id} color={nodeState.isFocused ? "blue" : undefined}>
                    {"  ".repeat(nodeState.depth)}
                    {nodeState.isExpanded ? "▾" : nodeState.hasChildren ? "▸" : " "} {node.label}
                </Text>
            ))}
        </Box>
    );
}

Theming

The default theme can be imported and inspected:

import { treeViewTheme } from "@visulima/tui/components/tree-view";
// treeViewTheme.styles.label({ isFocused: true, isSelected: false })
// => { color: "blue" }

For full visual customization, prefer renderNode over theme overrides.

Accordion

Keyboard-navigable accordion. Arrow keys (or j/k) move between panels; Enter or Space toggles.

PropTypeDefaultDescription
itemsAccordionItem[]requiredPanels ({ id, title, content })
allowMultiplebooleanfalseAllow multiple panels open at once
defaultExpandedreadonly string[]Ids open on mount
accentColorstring"blue"Color of the focused header
autoFocusbooleanfalseAuto-focus the first item on mount
isDisabledbooleanfalseDisable input
<Accordion items={[{ id: "one", title: "Section 1", content: <Text>Body</Text> }]} />

AnimatePresence

Orchestrates enter / exit animations for keyed children. Wrap a dynamic list of \<Transition /> (or any component accepting show: boolean + onExit) to keep outgoing children mounted until their exit animation finishes.

<AnimatePresence>
    {visible && (
        <Transition key="banner" preset="slide-down">
            <Text>Saved!</Text>
        </Transition>
    )}
</AnimatePresence>

Children must have a key to be tracked. Components opt in by exposing show + onExit props or by setting a static isAnimatable = true on the component type.

ApprovalPrompt

Three-way prompt for tool calls. y / Enter → allow-once, aallow-always, n / Esc → deny.

PropTypeDefaultDescription
toolstringrequiredName of the action under review
onDecision(d: ApprovalDecision) => voidrequiredResolution callback
risk"low" | "medium" | "high""medium"Severity badge / border color
descriptionstringBody text
paramsRecord\<string, unknown>Key=value preview
childrenReactNodeOptional body (diff, command preview)
accentColorstringrisk colorOverride option-letter color
autoFocusbooleantrueAuto-focus on mount
isDisabledbooleanfalseDisable input
<ApprovalPrompt tool="bash" risk="high" params={{ cmd: "rm -rf /" }} onDecision={resolve} />

AreaChart

Line chart with the area between each line and the baseline filled with shaded blocks.

PropTypeDefaultDescription
seriesLineSeries[]requiredSeries to plot (same shape as LineChart)
widthnumber40Plot width in columns
heightnumber10Plot height in rows
fillDensity"light" | "medium" | "heavy""medium"Glyph used for the fill
minY/maxYnumberautoY-axis bounds
palettestring[]defaultFallback colors
axisColorstring"gray"Axis label color
showLegendbooleantrueRender the legend
<AreaChart series={[{ name: "load", data: [1, 4, 2, 5, 3] }]} />

BarChart

Vertical or horizontal bar chart. Uses braille / block glyphs.

PropTypeDefaultDescription
dataBarDatum[]required{ value, label?, color? } per bar
orientation"vertical" | "horizontal""vertical"Bar orientation
widthnumberautoPlot width in columns
heightnumber10Plot height in rows
maxnumberautoUpper bound for the axis
palettestring[]defaultFallback colors
showLabelsbooleantrueRender labels under (or beside) each bar
showValuesbooleanfalseRender numeric values next to each bar
<BarChart
    data={[
        { value: 12, label: "Mon" },
        { value: 8, label: "Tue" },
    ]}
/>

BlinkDot

Steady or blinking indicator dot.

PropTypeDefaultDescription
characterstring"●"Glyph
colorstring"red"Color
intervalnumber800Milliseconds per blink cycle
isActivebooleantrueWhen false, the dot stays solid (no animation)
<BlinkDot color="green" /> Connected

Render a breadcrumb trail; the last item is highlighted as the current location.

PropTypeDefaultDescription
itemsBreadcrumbItem[]required{ label, icon?, key? }
separatorstring"›"Separator glyph
currentColorstring"blue"Color applied to last item
<Breadcrumb items={[{ label: "Home" }, { label: "Settings" }, { label: "Profile" }]} />

Button

Focusable button. Triggers onPress on Enter or Space.

PropTypeDefaultDescription
childrenReactNoderequiredLabel
onPress() => voidPressed callback
variant"primary" | "secondary" | "outline" | "ghost""primary"Visual style
accentColorstring"blue"Color when focused
autoFocusbooleanfalseFocus on mount
isDisabledbooleanfalseIgnores input and renders dim
<Button onPress={() => save()} autoFocus>
    Save
</Button>

Calendar

Month grid with keyboard navigation. Arrow keys move day-by-day; Enter submits.

PropTypeDefaultDescription
valueDateControlled selected date
defaultValueDatetodayInitial selection (uncontrolled)
onChange(d: Date) => voidCalled on navigation or selection
onSubmit(d: Date) => voidCalled on Enter
minDateDateLower bound
maxDateDateUpper bound
firstDayOfWeek0 | 11Sunday (0) or Monday (1)
accentColorstring"blue"Color of today / focused cell
selectedColorstring"green"Color of the selected day
autoFocusbooleanfalseAuto-focus on mount
isDisabledbooleanfalseDisable input
<Calendar onSubmit={(d) => setDate(d)} autoFocus />

Canvas

A fixed-size cell grid backed by typed arrays. The imperative draw callback paints into a CanvasContext; only rows the draw pass dirties are re-serialized. Bumping version invalidates the cache.

PropTypeDescription
widthnumberColumns (cells horizontally)
heightnumberRows
draw(ctx: CanvasContext) => voidDraw callback
versionunknownCache invalidation key (data hash, counter)
<Canvas width={20} height={5} version={frame} draw={(ctx) => ctx.setCell(0, 0, "█", { color: "cyan" })} />

This is the foundation for chart components (LineChart, BarChart, Heatmap, ScatterPlot).

Card

Container with optional title, subtitle, header-right, and footer.

PropTypeDefaultDescription
childrenReactNoderequiredBody
titlestringHeading
subtitlestringSub-heading
headerRightReactNodeContent rendered after the title
footerReactNodeFooter below body
borderColorstringBorder color
titleColorstringTitle color
borderStyleborder preset"round"Passed to Box
widthnumber | stringfillCard width
<Card title="Build" subtitle="Step 2 of 3" footer={<Text dimColor>Press q to cancel</Text>}>
    <Text>Bundling…</Text>
</Card>

Checkbox

Toggleable boolean input.

PropTypeDefaultDescription
isCheckedbooleanControlled checked state
defaultCheckedbooleanfalseInitial state (uncontrolled)
onChange(checked: boolean) => voidCalled on toggle
childrenReactNodeLabel
accentColorstring"blue"Color when focused
autoFocusbooleanfalseFocus on mount
isDisabledbooleanfalseDisable input
<Checkbox onChange={setEnabled}>Enable notifications</Checkbox>

Collapsible

Toggleable section with a focusable header.

PropTypeDefaultDescription
titleReactNoderequiredHeader label
childrenReactNoderequiredBody shown when open
defaultOpenbooleanfalseInitial state (uncontrolled)
isOpenbooleanControlled open state
onToggle(open: boolean) => voidCalled on toggle
accentColorstring"blue"Header color when focused
autoFocusbooleanfalseFocus on mount
isDisabledbooleanfalseDisable interaction

CommandBlock

Render a shell command and its output with a status header.

PropTypeDefaultDescription
commandstringrequiredCommand rendered in the header
status"idle" | "running" | "success" | "error""idle"Lifecycle state — drives icon and color
cwdstringWorking directory shown after command
outputReactNodeOutput (string or any node)
exitCodenumberExit code shown when finished
durationMsnumberDuration shown when finished
maxOutputRowsnumberTruncate output beyond this many rows

CommandPalette

Searchable command palette.

PropTypeDefaultDescription
commandsCommandEntry[]requiredEntries to search
onSelect(id: string, query: string) => voidrequiredSelection callback
onCancel() => voidCalled on Escape
limitnumber8Max rows
placeholderstring"Type a command or search..."Search field placeholder
emptyTextstring"No results"Shown when no matches
accentColorstring"cyan"Border / focused row color
autoFocusbooleanfalseAuto-focus on mount

ConfirmDialog

Modal confirm prompt with focusable buttons.

PropTypeDefaultDescription
onConfirm() => voidrequiredConfirm callback
onCancel() => voidrequiredCancel callback
titlestringHeading
childrenReactNodeBody content
confirmLabelstring"Confirm"Confirm button label
cancelLabelstring"Cancel"Cancel button label
defaultFocus"confirm" | "cancel""confirm"Initially focused button
tone"info" | "warning" | "danger""info"Border color
widthnumberDialog width
autoFocusbooleantrueAuto-focus on mount
isDisabledbooleanfalseDisable input

ContentSwitcher

Segmented control for picking among 2–5 options.

PropTypeDefaultDescription
optionsContentSwitcherOption[]requiredSegments ({ id, label, icon? })
valuestringControlled selected id
defaultValuestringfirstInitial selection (uncontrolled)
onChange(id: string) => voidCalled when the selection changes
accentColorstring"blue"Color of the selected segment
autoFocusbooleanfalseAuto-focus on mount
isDisabledbooleanfalseDisable input

DatePicker

Input row that opens an inline Calendar on focus / Enter.

PropTypeDefaultDescription
valueDateControlled selected date
defaultValueDateInitial date (uncontrolled)
onChange(d: Date) => voidCalled when the user commits
placeholderstring"Pick a date"Shown when no value is selected
formatValue(d: Date) => stringlocaleCustom display formatter
minDateDateLower bound
maxDateDateUpper bound
accentColorstring"blue"Calendar accent color
autoFocusbooleanfalseAuto-focus on mount
isDisabledbooleanfalseDisable input

DefinitionList

Term/definition pairs.

PropTypeDefaultDescription
itemsDefinitionItem[]required{ term, description } entries
layout"inline" | "stacked""inline"Inline = same row; stacked = term above
termColorstring"cyan"Color applied to the term
termWidthnumberMin width to align inline terms
columnGapnumber2Spacing between term and description columns

Dialog

Bordered, scrollable container that fills a fraction of the terminal. Used for modal-style overlays.

PropTypeDefaultDescription
childrenReactNoderequiredDialog body
titleReactNodeHeading
footerReactNodeFooter below the scrollable area
visiblebooleantrueRender only when truthy
widthnumber60Width in columns
maxHeightnumber0.8Fraction (≤1) or absolute rows
borderStyleborder preset"round"Passed to Box
borderColorstring"cyan"Border color
backgroundColorstringterminal bgDialog background
paddingXnumber2Inner horizontal padding
paddingYnumber1Inner vertical padding
scrollRefRef\<ScrollViewRef>Ref to the inner ScrollView
scrollbarStylescrollbar preset"block"Scrollbar visual style

Divider

Horizontal or vertical rule with an optional centered label.

PropTypeDefaultDescription
orientation"horizontal" | "vertical""horizontal"Direction
lengthnumberfillTotal length
characterstring/Override the line glyph
labelstringText rendered in the middle (horizontal only)
labelPaddingnumber1Padding around the label
colorstringLine color
dimColorbooleanfalseRender dim
<Divider label="Settings" />

Form

Layout primitive for forms — pairs nicely with useForm and the \<Form.Field> sub-component for label/error styling.

PropTypeDefaultDescription
childrenReactNoderequired\<Form.Field> rows (or any node)
titlestringHeading
descriptionstringSubtitle below the heading
footerReactNodeFooter (buttons, hints)
borderedbooleantrueRender with a border
accentColorstring"blue"Form chrome accent
widthnumberFixed width

Form.Field accepts { label, description?, error?, required?, isDisabled?, children } and renders consistent label + error styling around any control.

Gauge

Numeric range display. Useful for usage / load / capacity readouts.

PropTypeDefaultDescription
valuenumberrequiredCurrent value (clamped into [min, max])
minnumber0Lower bound
maxnumber100Upper bound
size"small" | "medium" | "large""medium"Dimension preset
labelstringLabel rendered above the readout
formatValue(v: number) => stringroundFormat the numeric readout
thresholdsGaugeThreshold[]Color thresholds (see source)
showValuebooleantrueShow the numeric readout in the middle
showLegendbooleanfalseShow a threshold legend below the gauge

Heading

Convenience wrapper around Text that picks color and decoration by level.

PropTypeDefaultDescription
childrenReactNoderequiredHeading content
level1 | 2 | 3 | 4 | 5 | 61Determines decoration / color
colorstringExplicit color override
underlinebooleanfalseRender an underline on a second line

Heatmap

2D matrix rendered as a colored cell grid.

PropTypeDefaultDescription
datanumber[][]requiredRows × cols matrix
cellWidthnumber2Width of each cell in columns
characterstring"█"Glyph painted in each cell
palettestring[]defaultColor ramp from low → high
min/maxnumberdataFixed bounds for consistent scales
rowLabelsstring[]Labels rendered to the left
columnLabelsstring[]Labels rendered below

(Type is HeatmapProps.)

Histogram

Bin numeric values into bars.

PropTypeDefaultDescription
datanumber[]requiredRaw values to bin
binsnumber10Number of equal-width buckets
thresholdsnumber[]Explicit bin edges (overrides bins)
min/maxnumberautoFixed min/max
widthnumberautoPlot width
heightnumber10Plot height
colorstringBar color
showValuesbooleanfalseShow the numeric count above each bar

Kbd

Render a single key cap.

PropTypeDefaultDescription
childrenReactNoderequiredKey label
colorstring"gray"Background color
variant"solid" | "outline" | "bare""solid"Visual style
Press <Kbd>Ctrl</Kbd>+<Kbd>C</Kbd> to copy.

LineChart

Multi-series line chart drawn on a Canvas braille grid.

PropTypeDefaultDescription
seriesLineSeries[]requiredSeries ({ name?, data, color? })
widthnumber40Plot width in columns
heightnumber10Plot height in rows
minY/maxYnumberautoY-axis bounds
palettestring[]defaultFallback colors
axisColorstring"gray"Axis tick color
showLegendbooleantrueShow legend

(Type is LineChartProps.)

LoadingIndicator

Spinner + optional inline message.

PropTypeDefaultDescription
typeSpinnerName"dots"@visulima/spinner preset
colorstring"cyan"Spinner and label color
childrenReactNodeOptional message rendered after the spinner
<LoadingIndicator>Building…</LoadingIndicator>

MaskedInput

Single-line input constrained by a literal mask. # (or a custom token) marks editable positions; every other character is rendered literally.

PropTypeDefaultDescription
maskstringrequiredMask, e.g. "(###) ###-####"
defaultValuestring""Initial raw value (no mask characters)
onChange(value: string) => voidCalled whenever the raw value changes
onSubmit(value: string) => voidCalled on Enter
tokenstring"#"Editable-position marker
placeholderCharstring"_"Character shown in empty positions
isDisabledbooleanfalseDisable input

Vertical menu with optional sections, icons, and shortcuts.

PropTypeDefaultDescription
onSelect(id: string) => voidrequiredSelection callback
itemsMenuItem[]Flat list (ignored if sections is set)
sectionsMenuSection[]Grouped items
titlestringHeader
borderedbooleantrueRender a border
accentColorstring"blue"Color of the focused row
autoFocusbooleanfalseAuto-focus on mount

MessageBubble

Conversation bubble with a role-based icon and color.

PropTypeDefaultDescription
childrenReactNoderequiredBubble body
role"user" | "assistant" | "system" | "tool""assistant"Determines alignment, color, icon
labelstringText rendered above the bubble
metastringMetadata next to the label (e.g. timestamp)
colorstringOverride the role color
flatbooleanfalseRender without a border

ModelBadge

Compact badge for model names.

PropTypeDefaultDescription
modelstringrequiredModel identifier
providerstringProvider chip rendered before the model
iconReactNodeOptional icon
colorstring"magenta"Background color
variant"solid" | "subtle" | "outline""solid"Visual style
<ModelBadge model="claude-opus-4.7" provider="Anthropic" />

OperationTree

Tree-shaped task list. Each node carries a status (pending, running, completed, error, skipped); running nodes show a spinner.

PropTypeDefaultDescription
nodesOperationNode[]requiredTree of operations
showSpinnerbooleantrueAnimate running nodes

OptionList

Static list with optional icons / descriptions / shortcuts. Non-interactive (use SelectInput or Menu for navigable lists).

PropTypeDefaultDescription
optionsOptionListEntry[]requiredEntries ({ id, label, ... })
currentIdstringId of the row to highlight
accentColorstringHighlight color
borderedbooleanfalseRender with a border
iconColumnWidthnumber2Icon column width for alignment

Paragraph

Block-level wrapper around Text with vertical margin. Useful inside long-form layouts.

PropTypeDefaultDescription
childrenReactNoderequiredParagraph body
colorstringForeground color
dimColorbooleanfalseRender dim
marginTopnumber0Blank lines before
marginBottomnumber1Blank lines after

Placeholder

Skeleton blocks for loading states.

PropTypeDefaultDescription
rowsnumber3Number of skeleton rows
widthsnumber[][1, 0.85, 0.6]Per-row width fractions (cycled)
widthnumberfillTotal width reference
characterstring"█"Fill character
colorstring"gray"Block color
animatedbooleantruePulse the blocks
intervalnumber500Pulse interval (ms)

RadioGroup

Single-choice list of options.

PropTypeDefaultDescription
optionsRadioOption[]requiredChoices ({ value, label, ... })
valuestringControlled selected value
defaultValuestringInitial value (uncontrolled)
onChange(value: string) => voidCalled when selection changes
onSubmit(value: string) => voidCalled on Enter
orientation"vertical" | "horizontal""vertical"Layout direction
commitOnNavigatebooleantrueIf true, arrow keys commit and fire onChange. If false, focus is independent and Space/Enter commits.
accentColorstring"blue"Color of focused / selected indicator
autoFocusbooleanfalseAuto-focus on mount
isDisabledbooleanfalseDisable input

ScatterPlot

Scatter chart drawn on a braille Canvas. Uses LineSeries (the data array supplies points).

PropTypeDefaultDescription
seriesLineSeries[]requiredSeries of points
widthnumber40Plot width
heightnumber10Plot height
minX/maxXnumberautoX-axis bounds
minY/maxYnumberautoY-axis bounds
palettestring[]defaultSeries fallback colors
axisColorstring"gray"Axis label color
showLegendbooleantrueShow legend

SearchInput

Single-line search input with leading icon and optional autocomplete suggestions.

PropTypeDefaultDescription
defaultValuestring""Initial value
onChange(value: string) => voidFired on every keystroke
onSubmit(value: string) => voidFired on Enter
placeholderstring"Search..."Empty-state text
iconReactNode"⌕"Leading icon
suggestionsstring[]Autocomplete candidates
accentColorstring"blue"Border / icon color
isDisabledbooleanfalseDisable input

SelectInputIndicator / SelectInputItem

Default sub-components used by \<SelectInput> for the indicator and item rendering. Exported so custom components can re-use the styling primitives, or so consumers can pass them through indicatorComponent/itemComponent props.

SelectInputIndicator accepts { isSelected?, isFocused?, accentColor? }.

SelectInputItem accepts { label, isSelected?, isFocused?, accentColor?, defaultColor? }.

ShimmerText

Text with a sweeping highlight band — a lightweight skeleton/loading effect.

PropTypeDefaultDescription
textstringrequiredText to render
bandWidthnumber3Width of the highlight in characters
intervalnumber60Frame interval (ms)
colorstringBase text color
highlightColorstring"white"Highlight band color

Sparkline

Tiny inline chart drawn with the eight Unicode block glyphs.

PropTypeDefaultDescription
datanumber[]requiredValues to plot
minnumberdataFixed minimum
maxnumberdataFixed maximum
colorstringGlyph color
<Sparkline data={[1, 4, 2, 5, 3, 7, 6]} color="cyan" />

StaticRender

Pre-renders its children once and caches the result on the underlying DOM node. The next layout pass composites the cached region directly via addRegionTree(), skipping the entire subtree traversal.

StaticRender is a featured perf primitive used heavily by vis for completed-task output, log lines, and other content that does not change.

<StaticRender deps={[completed.length]}>
    {() => (
        <Box flexDirection="column">
            {completed.map((task) => (
                <Text key={task.id} color="green">
                    ✓ {task.name}
                </Text>
            ))}
        </Box>
    )}
</StaticRender>
PropTypeDefaultDescription
children() => ReactNoderequiredRender function invoked once. The result is cached and composited on subsequent renders.
depsDependencyListReact-style dependency array (shallow Object.is comparison). When any entry changes the cache is invalidated and children() is invoked again. If omitted, the cache invalidates whenever the children function reference changes.
styleStylesStyle applied to the static-render container
widthnumberFixed width for the cached region

Pass a stable deps array (or memoize the children function) — otherwise a fresh function identity on every render forces a re-render.

Distinct from \<Static>, which is the append-only Ink primitive for completed log lines.

StatusLine

Single-row status bar with left / center / right slots.

PropTypeDefaultDescription
leftReactNodeContent rendered flush-left
centerReactNodeContent rendered centered
rightReactNodeContent rendered flush-right
colorstringDefault foreground
backgroundColorstringBackground spanning the row
<StatusLine left={<Text>main</Text>} right={<Text>UTF-8 · LF</Text>} />

Stepper

Progress indicator across discrete steps.

PropTypeDefaultDescription
stepsStepperStep[]requiredList of steps
activeIndexnumber0Index of the current step (when steps lack a status)
orientation"horizontal" | "vertical""horizontal"Layout direction
accentColorstring"blue"Color for active / completed steps
errorColorstring"red"Color for error steps

StreamingText

Reveal text one character at a time. Useful for typing-style output (LLM streaming, intros).

PropTypeDefaultDescription
textstringrequiredFull text to stream
intervalnumber20Milliseconds between characters
cursorstring"▊"Cursor character shown while streaming
showCursorWhenDonebooleanfalseKeep the cursor after streaming completes
onComplete() => voidCalled once per text value when complete
colorstringText color

Switch

Toggle switch with a labelled track.

PropTypeDefaultDescription
valuebooleanControlled value
defaultValuebooleanfalseInitial value (uncontrolled)
onChange(value: boolean) => voidCalled on toggle
childrenReactNodeLabel rendered after the switch
labels[string, string]["off", "on"]Track labels
accentColorstring"green"Color when on
autoFocusbooleanfalseFocus on mount
isDisabledbooleanfalseDisable input

Tag

Inline label / chip.

PropTypeDefaultDescription
childrenReactNoderequiredTag content
colorstring"blue"Base color
iconReactNodeOptional leading symbol
variant"subtle" | "solid" | "outline""subtle"Visual style

Toast

Auto-dismissing notification.

PropTypeDefaultDescription
childrenReactNoderequiredBody
titlestringOptional title before the body
variant"info" | "success" | "warning" | "error""info"Determines icon and border color
durationnumber4000Auto-dismiss timeout (ms); 0 disables
onDismiss() => voidCalled after auto-dismiss
visiblebooleanManually control visibility (disables duration)
widthnumberOptional fixed width

Tooltip

Inline tooltip attached to an anchor element. Terminal rendering does not support floating layers, so the tooltip is placed inline next to (or above / below) the anchor.

PropTypeDefaultDescription
childrenReactNoderequiredAnchor content
contentReactNoderequiredTooltip body
placement"top" | "right" | "bottom" | "left""right"Relative position
colorstring"gray"Border color
isVisiblebooleantrueRender only when truthy

Transition

Animate enter / exit of a child subtree. Pair with \<AnimatePresence> to keep outgoing children mounted until exit completes.

PropTypeDefaultDescription
childrenReactNoderequiredContent to animate
showbooleantrueDrive towards entered (true) or exited (false). Flip to trigger an animation.
preset"fade" | "slide-up" | "slide-down" | "slide-left" | "slide-right" | "reveal""fade"Animation preset
durationnumber240Total duration in milliseconds
distancenumber4Slide offset (slide presets) or row span (reveal)
tickIntervalnumber30Frame interval in milliseconds
onExit() => voidCalled once the exit animation finishes (used by \<AnimatePresence>)

On this page

ComponentsHeavy components (subpath imports)BoxOverflow and CSS-Level ScrollingSticky HeadersTextCursorInline mode (no props)Anchor modeCodeDiffViewSyntax highlighting in diffsFilePickerFilter optionsFileEntry shapeKeyboardBigTextGradientHelpShort modeFull modeLinkMarkdownStreaming modeSupported Markdown featuresNewlineSpacerStaticTransformSpinnerStopwatchRef methodsTimerRef methodsProgressBarSliderKeyboardControlled modeConsoleOverlayConfirmInputMultiSelectTextInputPassword modeAutocompleteTextareaKeyboardAuto-growing heightWith line numbersSelectInputPropsItem actionsSeparatorsNo initial selectionControlled modeScroll windowingCustom colorsFocus dimmingCustom componentsTab / TabsTabs PropsControlled modeTab PropsKeyboardColumn layoutCustom colorsTablePropsColumn ConfigurationBorder StylesCustom FormattingBadgeStatusMessageAlertUnorderedListOrderedListPaginatorPageMetaIndicator stylesKeyboardControlled modeTreeViewKeyboard controlsPropsTreeNode\<T> shapeCustom renderingAsync lazy-loadingViewport virtualizationHeadless usageThemingAccordionAnimatePresenceApprovalPromptAreaChartBarChartBlinkDotBreadcrumbButtonCalendarCanvasCardCheckboxCollapsibleCommandBlockCommandPaletteConfirmDialogContentSwitcherDatePickerDefinitionListDialogDividerFormGaugeHeadingHeatmapHistogramKbdLineChartLoadingIndicatorMaskedInputMenuMessageBubbleModelBadgeOperationTreeOptionListParagraphPlaceholderRadioGroupScatterPlotSearchInputSelectInputIndicator / SelectInputItemShimmerTextSparklineStaticRenderStatusLineStepperStreamingTextSwitchTagToastTooltipTransition
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