Hello World
Three ways to get your first OpenTUI app running: imperative (no framework), SolidJS, and React.
Prerequisites
Install Bun (>=1.3.0):
curl -fsSL https://bun.sh/install | bash
Option 1: Scaffolder (fastest)
bun create tui
cd my-tui
bun run dev
This creates a starter project with everything configured.
Option 2: Imperative API (no framework)
mkdir my-tui && cd my-tui
bun init -y
bun add @opentui/core
Create index.ts:
import { createCliRenderer, Box, Text } from "@opentui/core"
const renderer = await createCliRenderer({
exitOnCtrlC: true,
})
renderer.root.add(
Box(
{ borderStyle: "rounded", padding: 1, flexDirection: "column", gap: 1 },
Text({ content: "Hello, OpenTUI!", fg: "#00FF00" }),
Text({ content: "Press Ctrl+C to exit" }),
),
)
Run it:
bun index.ts
You get a bordered box with green text. Box() and Text() are construct functions - they create virtual nodes that become real components when added to the tree.
Option 3: SolidJS
mkdir my-tui && cd my-tui
bun init -y
bun add @opentui/core @opentui/solid solid-js
Create index.tsx:
import { render, useKeyboard, useRenderer } from "@opentui/solid"
import { createSignal } from "solid-js"
const App = () => {
const [count, setCount] = createSignal(0)
const renderer = useRenderer()
useKeyboard((key) => {
if (key.name === "space") setCount(c => c + 1)
if (key.name === "q") renderer.exit()
})
return (
<box style={{ padding: 2, flexDirection: "column", border: true, borderStyle: "rounded" }}>
<text content={`Count: ${count()}`} fg="#00FF00" />
<text content="Press SPACE to count, Q to quit" />
</box>
)
}
render(App)
Run it:
bun index.tsx
Gotcha: SolidJS JSX uses lowercase element names (
<box>,<text>, not<Box>,<Text>). Capital names would be treated as Solid components.
Option 4: React
mkdir my-tui && cd my-tui
bun init -y
bun add @opentui/core @opentui/react react
Create index.tsx:
import { createCliRenderer } from "@opentui/core"
import { createRoot, useKeyboard, useRenderer } from "@opentui/react"
import { useState } from "react"
const App = () => {
const [count, setCount] = useState(0)
const renderer = useRenderer()
useKeyboard((key) => {
if (key.name === "space") setCount(c => c + 1)
if (key.name === "q") renderer.exit()
})
return (
<box style={{ padding: 2, flexDirection: "column", border: true, borderStyle: "rounded" }}>
<text content={`Count: ${count}`} fg="#00FF00" />
<text content="SPACE to count, Q to quit" />
</box>
)
}
const renderer = await createCliRenderer({ exitOnCtrlC: true })
createRoot(renderer).render(<App />)
Run it:
bun index.tsx
Note the difference from Solid: React requires creating the renderer manually and passing it to createRoot(). Solid’s render() handles this internally.
What Just Happened
In all three cases:
createCliRenderer()loads the Zig native library, sets up the terminal (alternate screen, mouse tracking, raw mode), and returns a renderer- You built a component tree (either with constructs, Solid JSX, or React JSX)
- The renderer computed Yoga flexbox layout for all components
- The Zig core rendered the result to your terminal using diff-based updates
- Ctrl+C (or Q) triggered
renderer.destroy()which restored the terminal to its original state
Next Steps
- Components Guide - All the built-in components and how to use them
- Architecture - How the rendering pipeline works
- Testing - Write tests for your TUI app