| name | OpenTUI |
| description | Build terminal UIs with OpenTUI, a TypeScript library using Yoga-powered flexbox layout. Use when creating terminal applications, working with Text/Box/Input/Select/ScrollBox components, handling keyboard input, or writing OpenTUI tests. |
OpenTUI Development Guide
OpenTUI is a TypeScript library for building terminal user interfaces. It requires Bun as the runtime.
Installation
bun add @opentui/core
Or with React bindings:
bun add @opentui/react @opentui/core react
Core Concepts
Two APIs: Renderables vs Constructs
Renderables - Direct class instantiation with full control:
const text = new TextRenderable(renderer, {
id: "greeting",
content: "Hello!",
fg: "#00FF00",
});
Constructs - Functional composition (simpler syntax):
Text({ content: "Hello!", fg: "#00FF00" })
Layout System
OpenTUI uses Yoga (flexbox) for layout. Key properties:
flexDirection: "row" | "column"
justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-around"
alignItems: "flex-start" | "center" | "flex-end" | "stretch"
width / height: number or percentage string ("50%")
padding / gap: spacing in cells
flexGrow: number for proportional sizing
Colors
Use hex strings ("#FF0000") or RGBA values ({ r: 255, g: 0, b: 0, a: 255 }).
Components
Text
Display styled text with colors and attributes.
import { Text, t, bold, fg, italic } from "@opentui/core";
Text({ content: "Hello", fg: "#00FF00" })
Text({ content: t`${bold(fg("#FFFF00")("bold yellow"))} and ${italic("italic")}` })
Text Attributes: BOLD, DIM, ITALIC, UNDERLINE, BLINK, INVERSE, HIDDEN, STRIKETHROUGH (combine with bitwise OR)
Box
Container with borders and layout.
import { Box } from "@opentui/core";
Box({
border: "rounded",
title: "Panel Title",
titleAlign: "center",
padding: 1,
flexDirection: "column",
gap: 1,
},
Text({ content: "Child 1" }),
Text({ content: "Child 2" })
)
Input
Text input field requiring focus.
import { InputRenderable, InputEvent } from "@opentui/core";
const input = new InputRenderable(renderer, {
width: 30,
placeholder: "Type here...",
backgroundColor: "#333333",
focusedBackgroundColor: "#444444",
});
input.on(InputEvent.INPUT, () => console.log(input.value));
input.on(InputEvent.ENTER, () => handleSubmit(input.value));
Select
Vertical selection list requiring focus.
import { SelectRenderable, SelectEvent } from "@opentui/core";
const select = new SelectRenderable(renderer, {
options: [
{ name: "Option 1", description: "First option", value: "opt1" },
{ name: "Option 2", description: "Second option", value: "opt2" },
],
wrapSelection: true,
});
select.on(SelectEvent.ITEM_SELECTED, (index, option) => {
console.log(`Selected: ${option.value}`);
});
Keyboard: j/Down (next), k/Up (prev), Shift+Up/Down (fast scroll), Enter (select)
ScrollBox
Scrollable container for long content.
import { ScrollBoxRenderable } from "@opentui/core";
const scrollbox = new ScrollBoxRenderable(renderer, {
scrollY: true,
stickyScroll: "bottom",
viewportCulling: true,
});
scrollbox.scrollBy({ y: 10, unit: "line" });
scrollbox.scrollTo({ x: 0, y: 100 });
Keyboard (when focused): Arrow keys (line), Page Up/Down (viewport), Home/End (boundaries)
Code
Syntax-highlighted code display with Tree-sitter.
import { CodeRenderable, SyntaxStyle } from "@opentui/core";
const code = new CodeRenderable(renderer, {
content: 'console.log("Hello");',
filetype: "typescript",
syntaxStyle: SyntaxStyle.fromStyles({ keyword: { fg: "#FF79C6" } }),
streaming: true,
selectable: true,
});
React Integration
import { createCliRenderer } from "@opentui/core";
import { createRoot } from "@opentui/react";
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react";
const renderer = createCliRenderer();
const root = createRoot(renderer);
function App() {
const { width, height } = useTerminalDimensions();
useKeyboard((event) => {
if (event.key === "q") process.exit(0);
});
return <box border="rounded"><text>Terminal: {width}x{height}</text></box>;
}
root.render(<App />);
Available hooks: useRenderer(), useKeyboard(), useOnResize(), useTerminalDimensions(), useTimeline()
Testing
Use @opentui/core/testing for headless testing:
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
import { createTestRenderer } from "@opentui/core/testing";
describe("MyApp", () => {
let renderer, mockInput, mockMouse, renderOnce, captureCharFrame;
beforeEach(async () => {
const ctx = await createTestRenderer({
width: 80,
height: 24,
kittyKeyboard: true,
});
renderer = ctx.renderer;
mockInput = ctx.mockInput;
mockMouse = ctx.mockMouse;
renderOnce = ctx.renderOnce;
captureCharFrame = ctx.captureCharFrame;
});
afterEach(async () => {
await renderer.idle();
renderer.destroy();
});
it("should handle keyboard input", async () => {
await renderOnce();
mockInput.pressKey("j");
await renderOnce();
const frame = captureCharFrame();
expect(frame).toContain("expected text");
});
it("should handle mouse clicks", async () => {
await mockMouse.click(10, 5);
await renderOnce();
});
it("should handle modifier keys", async () => {
mockInput.pressKey("j", { super: true });
mockInput.pressKey("k", { ctrl: true });
mockInput.pressEnter();
await renderOnce();
});
});
Best Practices
-
Use await renderOnce() after state changes to ensure rendering completes before assertions
-
Use renderer.idle() in afterEach to wait for pending async operations
-
Structure components hierarchically - Box containers for layout, Text for content
-
Manage focus explicitly - Input and Select require focus to receive keyboard input
-
Use sticky scroll for chat-like interfaces where new content appears at bottom
-
Enable viewport culling on ScrollBox with many children for performance
-
Prefer Renderables for stateful components needing direct control, Constructs for simple composition
Documentation Links