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