React renderer for building terminal user interfaces using OpenTUI core
npm install @opentui/reactA React renderer for building terminal user interfaces using OpenTUI core. Create rich, interactive console applications with familiar React patterns and components.
Quick start with bun and create-tui:
``bash`
bun create tui --template react
Manual installation:
`bash`
bun install @opentui/react @opentui/core react
`tsx
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"
function App() {
return
}
const renderer = await createCliRenderer()
createRoot(renderer).render(
`
For optimal TypeScript support, configure your tsconfig.json:
`json`
{
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"jsxImportSource": "@opentui/react",
"strict": true,
"skipLibCheck": true
}
}
- Core Concepts
- Components
- Styling
- API Reference
- createRoot(renderer)
- render(element, config?)
- Hooks
- useRenderer()
- useKeyboard(handler, options?)
- useOnResize(callback)
- useTerminalDimensions()
- useTimeline(options?)
- Components
- Layout & Display Components
- Text Component
- Box Component
- Scrollbox Component
- ASCII Font Component
- Input Components
- Input Component
- Textarea Component
- Select Component
- Code & Diff Components
- Code Component
- Line Number Component
- Diff Component
- Examples
- Login Form
- Counter with Timer
- System Monitor Animation
- Styled Text Showcase
- Component Extension
- Using React DevTools
OpenTUI React provides several built-in components that map to OpenTUI core renderables:
Layout & Display:
-
-
-
-
Input Components:
- - Text input field
- - Multi-line text input field
- - Selection dropdown
-
Code & Diff Components:
- - Code block with syntax highlighting
-
-
Helpers:
- , , , , , , - Text modifiers (_must be used inside of the text component_)
Components can be styled using props or the style prop:
`tsx
// Direct props
// Style prop
`
Creates a root for rendering a React tree with the given CLI renderer.
`tsx
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"
const renderer = await createCliRenderer({
// Optional renderer configuration
exitOnCtrlC: false,
})
createRoot(renderer).render(
`
Parameters:
- renderer: A CliRenderer instance (typically created with createCliRenderer())
Returns: An object with a render method that accepts a React element.
> Deprecated: Use createRoot(renderer).render(node) instead.
Renders a React element to the terminal. This function is deprecated in favor of createRoot.
#### useRenderer()
Access the OpenTUI renderer instance.
`tsx
import { useRenderer } from "@opentui/react"
function App() {
const renderer = useRenderer()
useEffect(() => {
renderer.console.show()
console.log("Hello, from the console!")
}, [])
return
}
`
#### useKeyboard(handler, options?)
Handle keyboard events.
`tsx
import { useKeyboard } from "@opentui/react"
function App() {
useKeyboard((key) => {
if (key.name === "escape") {
process.exit(0)
}
})
return
}
`
Parameters:
- handler: Callback function that receives a KeyEvent objectoptions?
- : Optional configuration object:release?
- : Boolean to include key release events (default: false)
By default, only receives press events (including key repeats with repeated: true). Set options.release to true to also receive release events.
Example with release events:
`tsx
import { useKeyboard } from "@opentui/react"
import { useState } from "react"
function App() {
const [pressedKeys, setPressedKeys] = useState
useKeyboard(
(event) => {
setPressedKeys((keys) => {
const newKeys = new Set(keys)
if (event.eventType === "release") {
newKeys.delete(event.name)
} else {
newKeys.add(event.name)
}
return newKeys
})
},
{ release: true },
)
return (
)
}
`
#### useOnResize(callback)
Handle terminal resize events.
`tsx
import { useOnResize, useRenderer } from "@opentui/react"
import { useEffect } from "react"
function App() {
const renderer = useRenderer()
useEffect(() => {
renderer.console.show()
}, [renderer])
useOnResize((width, height) => {
console.log(Terminal resized to ${width}x${height})
})
return
}
`
#### useTerminalDimensions()
Get current terminal dimensions and automatically update when the terminal is resized.
`tsx
import { useTerminalDimensions } from "@opentui/react"
function App() {
const { width, height } = useTerminalDimensions()
return (
Terminal dimensions: {width}x{height}
)
}
`
Returns: An object with width and height properties representing the current terminal dimensions.
#### useTimeline(options?)
Create and manage animations using OpenTUI's timeline system. This hook automatically registers and unregisters the timeline with the animation engine.
`tsx
import { useTimeline } from "@opentui/react"
import { useEffect, useState } from "react"
function App() {
const [width, setWidth] = useState(0)
const timeline = useTimeline({
duration: 2000,
loop: false,
})
useEffect(() => {
timeline.add(
{
width,
},
{
width: 50,
duration: 2000,
ease: "linear",
onUpdate: (animation) => {
setWidth(animation.targets[0].width)
},
},
)
}, [])
return
}
`
Parameters:
- options?: Optional TimelineOptions object with properties:duration?
- : Animation duration in milliseconds (default: 1000)loop?
- : Whether the timeline should loop (default: false)autoplay?
- : Whether to automatically start the timeline (default: true)onComplete?
- : Callback when timeline completesonPause?
- : Callback when timeline is paused
Returns: A Timeline instance with methods:
- add(target, properties, startTime): Add animation to timelineplay()
- : Start the timelinepause()
- : Pause the timelinerestart()
- : Restart the timeline from beginning
#### Text Component
Display text with rich formatting.
`tsx
function App() {
return (
{/ Simple text /}
{/ Rich text with children /}
Red Text
{/ Text modifiers /}
Bold, Italic, and Underlined
)
}
`
#### Box Component
Container with borders and layout capabilities.
`tsx
function App() {
return (
{/ Basic box /}
{/ Box with title and styling /}
{/ Styled box /}
border: true,
width: 40,
height: 10,
margin: 1,
alignItems: "center",
justifyContent: "center",
}}
>
)
}
`
#### Scrollbox Component
A scrollable box.
`tsx
function App() {
return (
rootOptions: {
backgroundColor: "#24283b",
},
wrapperOptions: {
backgroundColor: "#1f2335",
},
viewportOptions: {
backgroundColor: "#1a1b26",
},
contentOptions: {
backgroundColor: "#16161e",
},
scrollbarOptions: {
showArrows: true,
trackOptions: {
foregroundColor: "#7aa2f7",
backgroundColor: "#414868",
},
},
}}
focused
>
{Array.from({ length: 1000 }).map((_, i) => (
style={{ width: "100%", padding: 1, marginBottom: 1, backgroundColor: i % 2 === 0 ? "#292e42" : "#2f3449" }}
>
} />`
))}
)
}
#### ASCII Font Component
Display ASCII art text with different font styles.
`tsx
import { useState } from "react"
function App() {
const text = "ASCII"
const [font, setFont] = useState<"block" | "shade" | "slick" | "tiny">("tiny")
return (
height: 8,
border: true,
marginBottom: 1,
}}
>
focused
onChange={(_, option) => setFont(option?.value)}
showScrollIndicator
options={[
{
name: "Tiny",
description: "Tiny font",
value: "tiny",
},
{
name: "Block",
description: "Block font",
value: "block",
},
{
name: "Slick",
description: "Slick font",
value: "slick",
},
{
name: "Shade",
description: "Shade font",
value: "shade",
},
]}
style={{ flexGrow: 1 }}
/>
)
}
`
#### Input Component
Text input field with event handling.
`tsx
import { useState } from "react"
function App() {
const [value, setValue] = useState("")
return (
placeholder="Type here..."
focused
onInput={setValue}
onSubmit={(value) => console.log("Submitted:", value)}
/>
)
}
`
#### Textarea Component
`tsx
import type { TextareaRenderable } from "@opentui/core"
import { useKeyboard, useRenderer } from "@opentui/react"
import { useEffect, useRef } from "react"
function App() {
const renderer = useRenderer()
const textareaRef = useRef
useEffect(() => {
renderer.console.show()
}, [renderer])
useKeyboard((key) => {
if (key.name === "return") {
console.log(textareaRef.current?.plainText)
}
})
return (
)
}
`
#### Select Component
Dropdown selection component.
`tsx
import type { SelectOption } from "@opentui/core"
import { useState } from "react"
function App() {
const [selectedIndex, setSelectedIndex] = useState(0)
const options: SelectOption[] = [
{ name: "Option 1", description: "Option 1 description", value: "opt1" },
{ name: "Option 2", description: "Option 2 description", value: "opt2" },
{ name: "Option 3", description: "Option 3 description", value: "opt3" },
]
return (
style={{ height: 22 }}
options={options}
focused={true}
onChange={(index, option) => {
setSelectedIndex(index)
console.log("Selected:", option)
}}
/>
)
}
`
#### Code Component
`tsx
import { RGBA, SyntaxStyle } from "@opentui/core"
const syntaxStyle = SyntaxStyle.fromStyles({
keyword: { fg: RGBA.fromHex("#ff6b6b"), bold: true }, // red, bold
string: { fg: RGBA.fromHex("#51cf66") }, // green
comment: { fg: RGBA.fromHex("#868e96"), italic: true }, // gray, italic
number: { fg: RGBA.fromHex("#ffd43b") }, // yellow
default: { fg: RGBA.fromHex("#ffffff") }, // white
})
const codeExample = function hello() {
// This is a comment
const message = "Hello, world!"
const count = 42
return message + " " + count
}
function App() {
return (
)
}
`
#### Line Number Component
Display code with line numbers, and optionally add diff highlights or diagnostic indicators.
`tsx
import type { LineNumberRenderable } from "@opentui/core"
import { RGBA, SyntaxStyle } from "@opentui/core"
import { useEffect, useRef } from "react"
function App() {
const lineNumberRef = useRef
const syntaxStyle = SyntaxStyle.fromStyles({
keyword: { fg: RGBA.fromHex("#C792EA") },
string: { fg: RGBA.fromHex("#C3E88D") },
number: { fg: RGBA.fromHex("#F78C6C") },
default: { fg: RGBA.fromHex("#A6ACCD") },
})
const codeContent = function fibonacci(n: number): number {
if (n <= 1) return n
return fibonacci(n - 1) + fibonacci(n - 2)
}
console.log(fibonacci(10))
useEffect(() => {
// Add diff highlight - line was added
lineNumberRef.current?.setLineColor(1, "#1a4d1a")
lineNumberRef.current?.setLineSign(1, { after: " +", afterColor: "#22c55e" })
// Add diagnostic indicator
lineNumberRef.current?.setLineSign(4, { before: "⚠️", beforeColor: "#f59e0b" })
}, [])
return (
fg="#6b7280"
bg="#161b22"
minWidth={3}
paddingRight={1}
showLineNumbers={true}
width="100%"
height="100%"
>
)
}
`
For a more complete example with interactive diff highlights and diagnostics, see examples/line-number.tsx.
#### Diff Component
Display unified or split-view diffs with syntax highlighting, customizable themes, and line number support. Supports multiple view modes (unified/split), word wrapping, and theme customization.
For a complete interactive example with theme switching and keybindings, see examples/diff.tsx.
`tsx
import { createCliRenderer } from "@opentui/core"
import { createRoot, useKeyboard } from "@opentui/react"
import { useCallback, useState } from "react"
function App() {
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
const [focused, setFocused] = useState<"username" | "password">("username")
const [status, setStatus] = useState("idle")
useKeyboard((key) => {
if (key.name === "tab") {
setFocused((prev) => (prev === "username" ? "password" : "username"))
}
})
const handleSubmit = useCallback(() => {
if (username === "admin" && password === "secret") {
setStatus("success")
} else {
setStatus("error")
}
}, [username, password])
return (
placeholder="Enter username..."
onInput={setUsername}
onSubmit={handleSubmit}
focused={focused === "username"}
/>
placeholder="Enter password..."
onInput={setPassword}
onSubmit={handleSubmit}
focused={focused === "password"}
/>
fg: status === "success" ? "green" : status === "error" ? "red" : "#999",
}}
>
{status.toUpperCase()}
)
}
const renderer = await createCliRenderer()
createRoot(renderer).render(
`
`tsx
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"
import { useEffect, useState } from "react"
function App() {
const [count, setCount] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setCount((prev) => prev + 1)
}, 1000)
return () => clearInterval(interval)
}, [])
return (
}
)
}
const renderer = await createCliRenderer()
createRoot(renderer).render(
`
`tsx
import { createCliRenderer, TextAttributes } from "@opentui/core"
import { createRoot, useTimeline } from "@opentui/react"
import { useEffect, useState } from "react"
type Stats = {
cpu: number
memory: number
network: number
disk: number
}
export const App = () => {
const [stats, setAnimatedStats] = useState
cpu: 0,
memory: 0,
network: 0,
disk: 0,
})
const timeline = useTimeline({
duration: 3000,
loop: false,
})
useEffect(() => {
timeline.add(
stats,
{
cpu: 85,
memory: 70,
network: 95,
disk: 60,
duration: 3000,
ease: "linear",
onUpdate: (values) => {
setAnimatedStats({ ...values.targets[0] })
},
},
0,
)
}, [])
const statsMap = [
{ name: "CPU", key: "cpu", color: "#6a5acd" },
{ name: "Memory", key: "memory", color: "#4682b4" },
{ name: "Network", key: "network", color: "#20b2aa" },
{ name: "Disk", key: "disk", color: "#daa520" },
]
return (
style={{
margin: 1,
padding: 1,
border: true,
marginLeft: 2,
marginRight: 2,
borderStyle: "single",
borderColor: "#4a4a4a",
}}
>
{statsMap.map((stat) => (
, height: 1, backgroundColor: stat.color }} />
))}
)
}
const renderer = await createCliRenderer()
createRoot(renderer).render(
`
`tsx
import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"
function App() {
return (
<>
Bold text
Underlined text
Red text
Blue text
Bold red text
Bold and blue combined
>
)
}
const renderer = await createCliRenderer()
createRoot(renderer).render(
`
You can create custom components by extending OpenTUIs base renderables:
`tsx
import {
BoxRenderable,
createCliRenderer,
OptimizedBuffer,
RGBA,
type BoxOptions,
type RenderContext,
} from "@opentui/core"
import { createRoot, extend } from "@opentui/react"
// Create custom component class
class ButtonRenderable extends BoxRenderable {
private _label: string = "Button"
constructor(ctx: RenderContext, options: BoxOptions & { label?: string }) {
super(ctx, {
border: true,
borderStyle: "single",
minHeight: 3,
...options,
})
if (options.label) {
this._label = options.label
}
}
protected renderSelf(buffer: OptimizedBuffer): void {
super.renderSelf(buffer)
const centerX = this.x + Math.floor(this.width / 2 - this._label.length / 2)
const centerY = this.y + Math.floor(this.height / 2)
buffer.drawText(this._label, centerX, centerY, RGBA.fromInts(255, 255, 255, 255))
}
set label(value: string) {
this._label = value
this.requestRender()
}
}
// Add TypeScript support
declare module "@opentui/react" {
interface OpenTUIComponents {
consoleButton: typeof ButtonRenderable
}
}
// Register the component
extend({ consoleButton: ButtonRenderable })
// Use in JSX
function App() {
return (
)
}
const renderer = await createCliRenderer()
createRoot(renderer).render(
`
OpenTUI React supports React DevTools for debugging your terminal applications. To enable DevTools integration:
1. Install the optional peer dependency:
`bash`
bun add --dev react-devtools-core@7
2. Start the standalone React DevTools:
`bash`
npx react-devtools@7
3. Run your app with the DEV environment variable:
`bash``
DEV=true bun run your-app.ts
After the app starts, you should see the component tree in React DevTools. You can inspect and modify props in real-time, and changes will be reflected immediately in your terminal UI.
When DevTools is connected, the WebSocket connection may prevent your process from exiting naturally.