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:

  1. createCliRenderer() loads the Zig native library, sets up the terminal (alternate screen, mouse tracking, raw mode), and returns a renderer
  2. You built a component tree (either with constructs, Solid JSX, or React JSX)
  3. The renderer computed Yoga flexbox layout for all components
  4. The Zig core rendered the result to your terminal using diff-based updates
  5. Ctrl+C (or Q) triggered renderer.destroy() which restored the terminal to its original state

Next Steps