@wolf-tui/react
v1.9.0
Published
React adapter for Wolfie
Maintainers
Readme
@wolf-tui/react
Build terminal UIs with React — flexbox layouts, styled components, keyboard input
Install · Quick Start · Components · Hooks · Theming · CSS Styling
The Problem
React terminal UI libraries exist (Ink), but wolf-tui's React adapter shares a layout engine and component library with Vue, Angular, Solid, and Svelte adapters. Same components, same Taffy-powered Flexbox + CSS Grid, same styling pipeline — across all five frameworks.
This package started as a fork of Ink, extended with the wolf-tui shared architecture, React Compiler integration, and the full @wolf-tui/plugin styling pipeline (Tailwind, SCSS, CSS Modules).
Install
Scaffold a new project (recommended)
npm create wolf-tui -- --framework reactGenerates a complete project with bundler config, TypeScript, and optional CSS tooling. See create-wolf-tui.
Manual setup
# Runtime dependencies
pnpm add @wolf-tui/react react
# Build tooling
pnpm add -D @wolf-tui/plugin @vitejs/plugin-react vite| Peer dependency | Version |
| --------------- | --------- |
| react | >= 19.0.0 |
Quick Start
import { render, Box, Text, useInput, useApp } from '@wolf-tui/react'
import { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const { exit } = useApp()
useInput((input, key) => {
if (key.upArrow) setCount((c) => c + 1)
if (key.downArrow) setCount((c) => Math.max(0, c - 1))
if (input === 'q') exit()
})
return (
<Box style={{ flexDirection: 'column', padding: 1 }}>
<Text style={{ color: 'green', fontWeight: 'bold' }}>
Counter: {count}
</Text>
<Text style={{ color: 'gray' }}>↑/↓ to change, q to quit</Text>
</Box>
)
}
render(<App />)For CSS class-based styling (
className="text-green p-1"), see CSS Styling.
render(element, options?)
Mounts a React element to the terminal.
const { rerender, unmount, waitUntilExit, clear } = render(<App />)
rerender(<App count={1} />) // Re-render with new props
unmount() // Unmount and exit
await waitUntilExit() // Wait for app to exit
clear() // Clear terminal output| Option | Type | Default | Description |
| ----------------------- | -------------------- | ---------------- | ------------------------- |
| stdout | NodeJS.WriteStream | process.stdout | Output stream |
| stdin | NodeJS.ReadStream | process.stdin | Input stream |
| stderr | NodeJS.WriteStream | process.stderr | Error stream |
| debug | boolean | false | Disable frame throttling |
| exitOnCtrlC | boolean | true | Exit on Ctrl+C |
| patchConsole | boolean | true | Patch console methods |
| maxFps | number | 30 | Maximum render frequency |
| isScreenReaderEnabled | boolean | env-based | Screen reader mode |
| incrementalRendering | boolean | false | Only update changed lines |
Components
Layout
| Component | Description | Key features |
| -------------- | ------------------------------------------- | ---------------------------------------------------------- |
| <Box> | Flexbox/Grid layout container | All CSS-like flex props, style object, className |
| <Text> | Styled inline text | Color, bold/italic/underline, wrap modes |
| <Newline> | Empty lines | count prop |
| <Spacer> | Fills remaining flex space | Pushes siblings apart in flex containers |
| <Static> | Renders items once, skips re-renders | Append-only logs, scroll-back history |
| <Transform> | Transforms rendered text of children | transform: (line, idx) => string |
| <ScrollView> | Fixed-height viewport with clipped overflow | Built-in arrow / PageUp / PageDown / Home / End navigation |
| <Table> | Box-drawing table for tabular data | ink-table parity, themable borders/cells, column subset |
Both accept style (inline object) and className (CSS classes via @wolf-tui/plugin).
Box style properties (passed via style):
| Property | Type | Description |
| ---------------- | ----------------------------------------------------------------------------- | ------------------- |
| flexDirection | 'row' \| 'column' \| 'row-reverse' \| 'column-reverse' | Flex direction |
| flexWrap | 'wrap' \| 'nowrap' \| 'wrap-reverse' | Flex wrap |
| flexGrow | number | Grow factor |
| flexShrink | number | Shrink factor |
| flexBasis | number \| string | Flex basis |
| alignItems | 'flex-start' \| 'center' \| 'flex-end' \| 'stretch' | Cross-axis |
| alignSelf | 'flex-start' \| 'center' \| 'flex-end' \| 'auto' | Self alignment |
| justifyContent | 'flex-start' \| 'center' \| 'flex-end' \| 'space-between' \| 'space-around' | Main-axis |
| gap | number | Gap between items |
| width | number \| string | Width |
| height | number \| string | Height |
| padding | number | Padding (all sides) |
| paddingX | number | Horizontal padding |
| paddingY | number | Vertical padding |
| margin | number | Margin (all sides) |
| marginX | number | Horizontal margin |
| marginY | number | Vertical margin |
| borderStyle | 'single' \| 'double' \| 'round' \| 'classic' | Border style |
| borderColor | string | Border color |
| overflow | 'visible' \| 'hidden' | Overflow behavior |
Text style properties (passed via style):
| Property | Type | Description |
| ----------------- | ---------------------------------------- | ---------------- |
| color | string | Text color |
| backgroundColor | string | Background color |
| fontWeight | 'bold' | Bold text |
| fontStyle | 'italic' | Italic text |
| textDecoration | 'underline' \| 'line-through' | Decoration |
| inverse | boolean | Inverse colors |
| textWrap | 'wrap' \| 'truncate' \| 'truncate-end' | Wrap mode |
Renders children inside a fixed-height viewport, clips overflow, and scrolls via marginTop: -offset. Built-in key bindings: ↑/↓ (row), PageUp/PageDown (viewport), Home/End. Adapted from ink-scroll-view.
| Prop | Type | Default | Description |
| ----------------------- | -------------------------- | ------- | -------------------------------------------------- |
| height | number | — | Viewport height in rows (required) |
| offset | number | — | Controlled scroll offset — omit for internal state |
| keyBindings | boolean | true | Enable arrows + page + home/end |
| onScroll | (offset: number) => void | — | Fires when offset changes |
| onContentHeightChange | (height: number) => void | — | Fires when measured content height changes |
Imperative handle (via ref<IScrollViewHandle>): scrollTo(offset), scrollBy(delta), scrollToTop(), scrollToBottom(), getScrollOffset(), getContentHeight(), getViewportHeight().
Display
| Component | Description | Key features |
| ----------------- | -------------------------------------- | -------------------------------------------------------------- |
| <Alert> | Boxed alert message | variant: success / error / warning / info + title |
| <Badge> | Inline coloured label | color prop, children = label |
| <Spinner> | Animated loading spinner | 80+ types (dots, line, arc, …), optional label |
| <ProgressBar> | Horizontal progress bar | value 0–100, custom characters, themable colors |
| <StatusMessage> | One-line status with icon | variant: success / error / warning / info |
| <ErrorOverview> | Formatted error display | Pretty stack trace, source frame highlight |
| <Gradient> | Coloured text gradient | 13 presets or custom hex stops, per-character interpolation |
| <BigText> | ASCII-art figlet-style banner | cfonts engine, multiple fonts, gradients, alignment |
| <Timer> | Count-up, countdown, or stopwatch | Lap recording, configurable format, drift-resistant |
| <TreeView> | Hierarchical tree with expand/collapse | Single/multi-select, async lazy loading, virtual scroll |
| <JsonViewer> | Interactive JSON tree viewer | 16 value types, syntax colouring, circular-reference detection |
| <FilePicker> | Filesystem browser with filter mode | Multi-select, symlinks, directory navigation |
Input
| Component | Description | Key features |
| ----------------- | ----------------------------------- | ------------------------------------------------------- |
| <TextInput> | Single-line text field | onChange / onSubmit, placeholder, mask, suggestions |
| <PasswordInput> | Masked text input | Configurable mask character |
| <EmailInput> | Email field with domain suggestions | Auto-completes top-100 email domains |
| <ConfirmInput> | Yes / No prompt | y / n keys, customizable defaults |
| <Select> | Single-selection picker | Keyboard nav, themed indicator, options array |
| <MultiSelect> | Multi-selection picker | Toggle with space, submit with enter |
| <Combobox> | Fuzzy-search autocomplete dropdown | Two-pass fzf-style matching, cursor nav, autofill |
Lists
| Component | Description | Key features |
| ----------------- | ------------- | ------------------------------ |
| <OrderedList> | Numbered list | <OrderedListItem> children |
| <UnorderedList> | Bulleted list | <UnorderedListItem> children |
// Alert (children as message)
<Alert variant="success" title="Deployed">
All services are running.
</Alert>
// Badge (children as label)
<Badge color="green">NEW</Badge>
// StatusMessage (children as message)
<StatusMessage variant="success">Saved!</StatusMessage>
// TextInput
<TextInput
placeholder="Your name..."
onChange={(value) => console.log(value)}
onSubmit={(value) => console.log('done:', value)}
/>
// Select
<Select
options={[
{ label: 'TypeScript', value: 'ts' },
{ label: 'JavaScript', value: 'js' },
]}
onChange={(value) => console.log('Picked:', value)}
/>
// ProgressBar
<ProgressBar value={75} />
// Spinner
<Spinner type="dots" label="Loading..." />
// Lists
<OrderedList>
<OrderedListItem>First</OrderedListItem>
<OrderedListItem>Second</OrderedListItem>
</OrderedList>
// Timer (countdown)
<Timer variant="countdown" durationMs={60000} format="human" onComplete={() => console.log('done')} />
// TreeView
<TreeView
data={[{ id: '1', label: 'src', children: [{ id: '1.1', label: 'index.ts' }] }]}
selectionMode="single"
onSelectChange={(ids) => console.log(ids)}
/>
// Combobox
<Combobox
options={[{ label: 'TypeScript', value: 'ts' }, { label: 'JavaScript', value: 'js' }]}
placeholder="Search..."
onSelect={(value) => console.log(value)}
/>
// JsonViewer
<JsonViewer data={{ name: 'wolf-tui', version: '1.0' }} defaultExpandDepth={2} />
// FilePicker
<FilePicker initialPath="." multiSelect onSelect={(paths) => console.log(paths)} />
// Table (ink-table parity)
<Table data={[{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]} columns={['id', 'name']} padding={1} />
// ScrollView — uncontrolled, built-in arrows/PageUp/PageDown/Home/End
<ScrollView height={8} onScroll={(o) => console.log('offset', o)}>
{items.map((it, i) => <Text key={i}>{it}</Text>)}
</ScrollView>
// ScrollView — controlled + imperative handle
const ref = useRef<IScrollViewHandle>(null)
<ScrollView height={8} offset={offset} onScroll={setOffset} ref={ref} />
// ref.current?.scrollToBottom()
// Gradient — by preset name
<Gradient name="rainbow">wolf-tui in color</Gradient>
// Gradient — custom stops
<Gradient colors={['#ff3366', '#ffd700']}>Hand-picked stops</Gradient>Hooks
useInput(handler, options?)
Handle keyboard input.
import { useInput } from '@wolf-tui/react'
function App() {
useInput((input, key) => {
if (key.upArrow) {
/* move up */
}
if (key.return) {
/* confirm */
}
if (input === 'q') {
/* quit */
}
})
return <Text>Press q to quit</Text>
}| Property | Type | Description |
| ------------ | --------- | ------------------- |
| upArrow | boolean | Up arrow pressed |
| downArrow | boolean | Down arrow pressed |
| leftArrow | boolean | Left arrow pressed |
| rightArrow | boolean | Right arrow pressed |
| return | boolean | Enter pressed |
| escape | boolean | Escape pressed |
| ctrl | boolean | Ctrl held |
| shift | boolean | Shift held |
| meta | boolean | Meta key held |
| tab | boolean | Tab pressed |
| backspace | boolean | Backspace pressed |
| delete | boolean | Delete pressed |
| pageUp | boolean | Page Up pressed |
| pageDown | boolean | Page Down pressed |
The isActive option (boolean) enables/disables input handling.
useApp()
Access the app context — primarily for exit().
const { exit } = useApp()useFocus(options?) / useFocusManager()
Make components focusable and control focus programmatically.
const { isFocused } = useFocus({ autoFocus: true })
const { focusNext, focusPrevious } = useFocusManager()Stream access
| Hook | Returns |
| ------------- | ------------------------------------------- |
| useStdin() | { stdin, setRawMode, isRawModeSupported } |
| useStdout() | { stdout, write } |
| useStderr() | { stderr, write } |
Accessibility
| Hook | Returns | Notes |
| ---------------------------- | --------- | -------------------------------------------- |
| useIsScreenReaderEnabled() | boolean | Render alternative output for screen readers |
import { useIsScreenReaderEnabled, Text } from '@wolf-tui/react'
function MyView() {
const srEnabled = useIsScreenReaderEnabled()
return <Text>{srEnabled ? 'Welcome, screen reader user' : 'Welcome'}</Text>
}Spinner animation hook for building custom spinners:
import { useSpinner } from '@wolf-tui/react'
function Loading() {
const { frame } = useSpinner({ type: 'dots' })
return <Text>{frame} Loading...</Text>
}Theming
Customize component appearance via ThemeProvider:
import {
render,
ThemeProvider,
extendTheme,
defaultTheme,
} from '@wolf-tui/react'
const theme = extendTheme(defaultTheme, {
components: {
Spinner: { styles: { spinner: { color: 'cyan' } } },
Alert: { styles: { container: { borderColor: 'blue' } } },
},
})
render(
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
)| Export | Description |
| ------------------------------ | ---------------------------------------------- |
| ThemeProvider | React context provider for theme |
| extendTheme(base, overrides) | Deep-merge overrides into base theme |
| defaultTheme | Base theme object |
| useComponentTheme(name) | Read theme for a component (inside components) |
CSS Styling
Three approaches, all via @wolf-tui/plugin:
| Method | Usage |
| ------------- | -------------------------------------- |
| Inline styles | style={{ color: 'green' }} |
| Tailwind CSS | className="text-green p-1" + PostCSS |
| CSS Modules | className={styles.box} |
All CSS approaches resolve to terminal styles at build time — no runtime CSS engine.
Tailwind CSS:
import './styles.css'
;<Box className="flex-col p-4 gap-2">
<Text className="text-green-500 font-bold">Tailwind styled</Text>
</Box>CSS Modules:
import styles from './App.module.css'
;<Box className={styles.container}>
<Text className={styles.title}>CSS Modules</Text>
</Box>React Compiler
@wolf-tui/react ships pre-compiled with the React Compiler — all library components skip re-renders when props haven't changed. To apply the same optimization to your own components:
pnpm add -D babel-plugin-react-compiler// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { wolfie } from '@wolf-tui/plugin/vite'
export default defineConfig({
plugins: [
react({
babel: {
plugins: [['babel-plugin-react-compiler', {}]],
},
}),
wolfie('react'),
],
})Requires React 19+.
Part of wolf-tui
This is the React adapter for wolf-tui — a framework-agnostic terminal UI library. The same layout engine (Taffy/flexbox) and component render functions power adapters for Vue, Angular, Solid, and Svelte.
The examples/ directory has working setups for each bundler:
| Bundler | Example |
| ------- | ------------------------- |
| Vite | examples/react_vite/ |
| esbuild | examples/react_esbuild/ |
| webpack | examples/react_webpack/ |
License
MIT
