Gotchas

Collected pitfalls from OpenTUI’s docs and codebase. Check here first when something behaves unexpectedly.

Terminal Compatibility

OSC 66 Artifacts

Symptom: Weird artifacts containing “66” in your terminal output.

Cause: Your terminal does not support OSC 66 escape sequences (used for explicit character width detection).

Affected terminals: GNOME Terminal, older Konsole, older xterm, most VT100/VT220 emulators.

Fix:

export OPENTUI_FORCE_EXPLICIT_WIDTH=false

Or in code:

process.env.OPENTUI_FORCE_EXPLICIT_WIDTH = "false"
const renderer = await createCliRenderer()

Not affected: Kitty, Ghostty, WezTerm, Alacritty, iTerm2 all support OSC 66.

Runtime

No console.log at Runtime

Symptom: You add console.log() but see no output.

Cause: OpenTUI captures stdout. The terminal is in alternate screen mode showing only the TUI.

Fix: Toggle the built-in console overlay with backtick (`). Or use environment variables:

SHOW_CONSOLE=true bun index.ts     # open console overlay at startup
OTUI_USE_CONSOLE=false bun index.ts  # disable console capture entirely

For debugging during development, write to a log file:

OTUI_LOG_FILE=/tmp/opentui.log bun index.ts

Terminal Not Restored After Crash

Symptom: After your app crashes, the terminal is in a broken state (no visible cursor, raw mode active, alternate screen still showing).

Cause: renderer.destroy() was never called. OpenTUI does not auto-cleanup on process.exit.

Fix: Run reset in your terminal. Then add error handlers to your app:

const renderer = await createCliRenderer()

process.on("uncaughtException", (error) => {
  renderer.destroy()
  console.error(error)
  process.exit(1)
})

process.on("unhandledRejection", (reason) => {
  renderer.destroy()
  console.error(reason)
  process.exit(1)
})

Bun-Only (For Now)

Symptom: Import errors when running with Node.js or Deno.

Cause: OpenTUI uses bun:ffi for native library loading. Node.js and Deno support is in progress but not available yet.

Fix: Use Bun (>=1.3.0). There is no workaround for other runtimes currently.

Components

JSX Element Names Are Lowercase

Symptom: <Box> or <Text> in JSX doesn’t work as expected.

Cause: In both SolidJS and React integrations, OpenTUI components are intrinsic elements (lowercase). Capital names are interpreted as user-defined components.

Fix: Always use lowercase:

// Correct
<box style={{ padding: 1 }}>
  <text content="Hello" />
</box>

// Wrong - treated as a Solid/React component named Box
<Box style={{ padding: 1 }}>
  <Text content="Hello" />
</Box>

Style Prop Is Not CSS

Symptom: CSS property names or concepts do not work as expected.

Cause: The style prop is a convenience bag that sets properties directly on the renderable instance. There is no CSS cascade, no specificity, no inheritance, no computed styles.

// This works (direct properties mapped to renderable)
<box style={{ padding: 2, flexDirection: "column", border: true }} />

// This does NOT work (CSS concepts that don't exist)
<box style={{ display: "grid", fontSize: 14, boxShadow: "..." }} />

Text Nodes Must Be Inside <text>

Symptom: Error or invisible content when placing raw strings in the tree.

Cause: In the React integration, text instances (bare strings) must be children of a <text> element. The reconciler checks isInsideText context.

Fix:

// Wrong
<box>Hello</box>

// Correct
<box><text content="Hello" /></box>

Tip: This restriction is enforced in React. SolidJS is more lenient with text node placement.

Development

Zig Required for Native Changes Only

Symptom: Confusion about when to rebuild.

Rule: If you only changed TypeScript, you do NOT need to run bun run build. The build is only needed when changing Zig source files in packages/core/src/zig/.

Prebuilt Binaries

Published npm packages include prebuilt native libraries for:

  • darwin-x64, darwin-arm64
  • linux-x64, linux-arm64
  • win32-x64, win32-arm64

If you are just using @opentui/core (not developing it), you do not need Zig installed. Zig is only needed for building from source.

Memory

Native Memory Leaks

Symptom: Memory usage grows over time.

Cause: The Zig allocator has no garbage collector. If TypeScript creates native objects (TextBuffer, EditBuffer, etc.) via FFI and does not call the corresponding destroy functions, the memory leaks.

Fix: Always call destroyRecursively() when removing components from the tree. Both React and Solid reconcilers do this automatically on unmount - but if you are using the imperative API, you must handle it:

// Imperative: must clean up manually
const text = new TextRenderable(renderer, { id: "temp", content: "hi" })
renderer.root.add(text)
// ... later ...
renderer.root.remove("temp")
text.destroy()  // or text.destroyRecursively() for children too