# Table of Contents
- [Getting started](#getting-started)
- [Renderables vs Constructs](#renderables-vs-constructs)
- [Renderables](#renderables)
- [Lifecycle and cleanup](#lifecycle-and-cleanup)
- [Layout System](#layout-system)
- [Constructs](#constructs)
- [Keyboard input](#keyboard-input)
- [Renderer](#renderer)
- [Console overlay](#console-overlay)
- [Colors](#colors)
- [Plugin Slots](#plugin-slots)
- [React](#react)
- [Text](#text)
- [Core](#core)
- [Solid](#solid)
- [Textarea](#textarea)
- [Input](#input)
- [Select](#select)
- [Box](#box)
- [TabSelect](#tabselect)
- [ScrollBar](#scrollbar)
- [Slider](#slider)
- [Markdown](#markdown)
- [FrameBuffer](#framebuffer)
- [Code](#code)
- [Diff](#diff)
- [Line numbers](#line-numbers)
- [Environment variables](#environment-variables)
- [Solid.js](#solid-js)
- [ASCIIFont](#asciifont)
- [Tree-sitter](#tree-sitter)
- [React](#react)
- [ScrollBox](#scrollbox)
- [Color matrix](#color-matrix)
---
# Getting started
Getting started
===============
OpenTUI is a native terminal UI core written in Zig with TypeScript bindings. The native core exposes a C ABI and can be used from any language. OpenTUI powers OpenCode in production today and will also power terminal.shop. It is an extensible core with a focus on correctness, stability, and high performance. It provides a component-based architecture with flexible layout capabilities, allowing you to create complex terminal applications.
Installation
------------
OpenTUI is currently [Bun](https://bun.sh/)
exclusive but Deno and Node support in-progress.
mkdir my-tui && cd my-tui
bun init -y
bun add @opentui/core
Hello world
-----------
Create `index.ts`:
import { createCliRenderer, Text } from "@opentui/core"
const renderer = await createCliRenderer({
exitOnCtrlC: true,
})
renderer.root.add(
Text({
content: "Hello, OpenTUI!",
fg: "#00FF00",
}),
)
Run it:
bun index.ts
You should see green text. Press `Ctrl+C` to exit.
Composing components
--------------------
Components nest naturally. Here’s a bordered panel with content:
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: "Welcome", fg: "#FFFF00" }),
Text({ content: "Press Ctrl+C to exit" }),
),
)
`Box` and `Text` are factory functions. The first argument is props; additional arguments are children.
What’s next
-----------
### Core concepts
* [Renderer](https://opentui.com/docs/core-concepts/renderer)
- The rendering engine
* [Layout](https://opentui.com/docs/core-concepts/layout)
- Flexbox positioning
* [Constructs](https://opentui.com/docs/core-concepts/constructs)
- The declarative component API
### Components
* [Text](https://opentui.com/docs/components/text)
, [Box](https://opentui.com/docs/components/box)
- Display and layout
* [Input](https://opentui.com/docs/components/input)
, [Select](https://opentui.com/docs/components/select)
- User interaction
### Framework bindings
* [React](https://opentui.com/docs/bindings/react)
* [Solid.js](https://opentui.com/docs/bindings/solid)
---
# Renderables vs Constructs
Renderables vs Constructs
=========================
OpenTUI provides two ways to build your UI: the imperative Renderable API and the declarative Construct API. Both approaches have different tradeoffs.
Imperative (Renderables)
------------------------
You create `Renderable` instances with a `RenderContext` and compose them using `add()`. You mutate state and behavior directly on instances through setters and methods.
import { BoxRenderable, TextRenderable, InputRenderable, createCliRenderer, type RenderContext } from "@opentui/core"
const renderer = await createCliRenderer()
const loginForm = new BoxRenderable(renderer, {
id: "login-form",
width: 40,
height: 10,
padding: 1,
})
// Compose multiple renderables into one
function createLabeledInput(renderer: RenderContext, props: { label: string; placeholder: string; id: string }) {
const container = new BoxRenderable(renderer, {
id: `${props.id}-container`,
flexDirection: "row",
})
container.add(
new TextRenderable(renderer, {
id: `${props.id}-label`,
content: props.label + " ",
}),
)
container.add(
new InputRenderable(renderer, {
id: `${props.id}-input`,
placeholder: props.placeholder,
width: 20,
}),
)
return container
}
const username = createLabeledInput(renderer, {
id: "username",
label: "Username:",
placeholder: "Enter username...",
})
loginForm.add(username)
// You must navigate to the nested component to focus it
username.getRenderable("username-input")?.focus()
renderer.root.add(loginForm)
### Characteristics
* Requires `RenderContext` at creation time
* Direct mutation of instances
* Manual navigation for nested component access
* Explicit control over component lifecycle
Declarative (Constructs)
------------------------
Builds a lightweight VNode graph using functional constructs. Instances don’t exist until you add the node to the tree. VNodes queue method calls and replay them when instantiated.
import { Text, Input, Box, createCliRenderer, delegate } from "@opentui/core"
const renderer = await createCliRenderer()
function LabeledInput(props: { id: string; label: string; placeholder: string }) {
return delegate(
{ focus: `${props.id}-input` },
Box(
{ flexDirection: "row" },
Text({ content: props.label + " " }),
Input({
id: `${props.id}-input`,
placeholder: props.placeholder,
width: 20,
}),
),
)
}
const usernameInput = LabeledInput({
id: "username",
label: "Username:",
placeholder: "Enter username...",
})
// delegate() automatically routes focus to the nested input
usernameInput.focus()
const loginForm = Box(
{ width: 40, height: 10, padding: 1 },
usernameInput,
LabeledInput({
id: "password",
label: "Password:",
placeholder: "Enter password...",
}),
)
renderer.root.add(loginForm)
### Characteristics
* No `RenderContext` needed until instantiation
* VNodes queue method calls
* `delegate()` routes APIs to nested components
* Declarative, React-like syntax
The delegate() function
-----------------------
The `delegate()` function makes constructs ergonomic by routing method calls from the parent to specific children:
function Button(props: { id: string; label: string; onClick: () => void }) {
return delegate(
{
focus: `${props.id}-box`, // Route focus() to the box
},
Box(
{
id: `${props.id}-box`,
border: true,
onMouseDown: props.onClick,
},
Text({ content: props.label }),
),
)
}
const button = Button({ id: "submit", label: "Submit", onClick: handleSubmit })
button.focus() // Focuses the inner Box
When to use which
-----------------
### Use Renderables when
* You need fine-grained control over component lifecycle
* You’re building low-level custom components
* You need to access renderable methods immediately
* Performance is critical and you want to avoid VNode overhead
### Use Constructs when
* You prefer declarative, compositional code
* You’re building higher-level UI components
* You want cleaner, more readable component definitions
* You’re familiar with React/Solid patterns
Mixing both
-----------
You can mix both approaches in the same application:
import { BoxRenderable, Text, Input } from "@opentui/core"
// Create a renderable container
const container = new BoxRenderable(renderer, {
id: "container",
flexDirection: "column",
})
// Add constructs to it
container.add(Text({ content: "Title" }), Input({ placeholder: "Type here..." }))
renderer.root.add(container)
---
# Renderables
Renderables
===========
Renderables are the building blocks of your UI. You can position, style, and nest them within each other. Each renderable represents a visual element and uses the Yoga layout engine for flexible positioning and sizing.
Creating Renderables
--------------------
Create a renderable by instantiating a class with a render context (the renderer) and options:
import { createCliRenderer, TextRenderable, BoxRenderable } from "@opentui/core"
const renderer = await createCliRenderer()
const greeting = new TextRenderable(renderer, {
id: "greeting",
content: "Hello, OpenTUI!",
fg: "#00FF00",
})
renderer.root.add(greeting)
Available Renderables
---------------------
OpenTUI provides these built-in renderables:
| Class | Description |
| --- | --- |
| `BoxRenderable` | Container with border, background, and layout |
| `TextRenderable` | Read-only styled text display |
| `InputRenderable` | Single-line text input |
| `TextareaRenderable` | Multi-line editable text |
| `SelectRenderable` | Dropdown/list selection |
| `TabSelectRenderable` | Horizontal tab selection |
| `ScrollBoxRenderable` | Scrollable container |
| `ScrollBarRenderable` | Standalone scroll bar control |
| `CodeRenderable` | Syntax-highlighted code display |
| `LineNumberRenderable` | Line number gutter for code/text views |
| `DiffRenderable` | Unified or split diff viewer |
| `ASCIIFontRenderable` | ASCII art font display |
| `FrameBufferRenderable` | Raw framebuffer for custom graphics |
| `MarkdownRenderable` | Markdown renderer |
| `SliderRenderable` | Numeric slider control |
The Renderable Tree
-------------------
Renderables form a tree structure. Use `add()` and `remove()` to manage children:
const container = new BoxRenderable(renderer, {
id: "container",
flexDirection: "column",
padding: 1,
})
const title = new TextRenderable(renderer, { id: "title", content: "My App" })
const body = new TextRenderable(renderer, { id: "body", content: "Content here" })
container.add(title)
container.add(body)
renderer.root.add(container)
// Later, remove a child
container.remove("body")
Finding Renderables
-------------------
Navigate the tree to find specific renderables:
// Get a direct child by ID
const title = container.getRenderable("title")
// Recursively search all descendants
const deepChild = container.findDescendantById("nested-input")
// Get all children
const children = container.getChildren()
Layout Properties
-----------------
All renderables support Yoga flexbox properties:
const panel = new BoxRenderable(renderer, {
id: "panel",
// Sizing
width: 40,
height: "50%",
minWidth: 20,
maxHeight: 30,
// Flex behavior
flexGrow: 1,
flexShrink: 0,
flexDirection: "column",
justifyContent: "center",
alignItems: "flex-start",
// Positioning
position: "absolute",
left: 10,
top: 5,
// Spacing
padding: 2,
paddingTop: 1,
margin: 1,
})
See the [Layout](https://opentui.com/docs/core-concepts/layout)
page for complete details.
Focus Management
----------------
Interactive renderables can receive keyboard focus:
const input = new InputRenderable(renderer, {
id: "username",
placeholder: "Enter username...",
})
renderer.root.add(input)
// Give focus to the input
input.focus()
// Remove focus
input.blur()
// Check focus state
console.log(input.focused) // true
By default, left-clicking a renderable will auto-focus the closest focusable ancestor. Disable this globally with `createCliRenderer({ autoFocus: false })`, or stop it per interaction by calling `event.preventDefault()` in `onMouseDown`.
Listen for focus changes:
import { RenderableEvents } from "@opentui/core"
input.on(RenderableEvents.FOCUSED, () => {
console.log("Input focused")
})
input.on(RenderableEvents.BLURRED, () => {
console.log("Input blurred")
})
Event Handling
--------------
### Mouse Events
Handle mouse interactions via options:
const button = new BoxRenderable(renderer, {
id: "button",
border: true,
onMouseDown: (event) => {
console.log("Clicked at", event.x, event.y)
},
onMouseOver: (event) => {
button.borderColor = "#FFFF00"
},
onMouseOut: (event) => {
button.borderColor = "#FFFFFF"
},
})
Available mouse events:
* `onMouseDown`, `onMouseUp`
* `onMouseMove`, `onMouseDrag`, `onMouseDragEnd`, `onMouseDrop`
* `onMouseOver`, `onMouseOut`
* `onMouseScroll`
* `onMouse` (catch-all)
Mouse events bubble up through the tree. Stop propagation with `event.stopPropagation()`.
### Keyboard Events
For focusable renderables:
const textDecoder = new TextDecoder()
const input = new InputRenderable(renderer, {
id: "input",
onKeyDown: (key) => {
if (key.name === "escape") {
input.blur()
}
},
onPaste: (event) => {
console.log("Pasted:", textDecoder.decode(event.bytes))
},
})
Visibility
----------
Control visibility with the `visible` property:
// Hide (also removes from layout)
panel.visible = false
// Show
panel.visible = true
When `visible` is `false`, Yoga excludes the renderable from layout calculation (equivalent to CSS `display: none`).
Opacity
-------
Set opacity for semi-transparent rendering:
panel.opacity = 0.5 // 50% transparent
Opacity affects the renderable and all its children.
Z-Index
-------
Control layering order for overlapping elements:
const overlay = new BoxRenderable(renderer, {
id: "overlay",
position: "absolute",
zIndex: 100, // Higher values render on top
})
Live Rendering
--------------
For animations, extend the Renderable class and override `onUpdate`:
class AnimatedBox extends BoxRenderable {
onUpdate(deltaTime) {
// Update animation state
this.translateX += 1
}
}
const box = new AnimatedBox(renderer, {
id: "anim-box",
live: true, // Enable continuous rendering
})
Translation
-----------
Offset a renderable from its layout position (useful for scrolling/animation):
// Offset by pixels
renderable.translateX = 10
renderable.translateY = -5
This moves the renderable visually without affecting layout.
Buffered Rendering
------------------
Enable offscreen rendering for complex content and use hooks to draw to the buffer:
import { RGBA } from "@opentui/core"
const complex = new BoxRenderable(renderer, {
id: "complex",
buffered: true, // Render to offscreen buffer first
renderAfter: (buffer) => {
// Draw directly to the buffer (or offscreen buffer if buffered=true)
buffer.fillRect(0, 0, 10, 5, RGBA.fromHex("#FF0000"))
},
})
Lifecycle Methods
-----------------
Override these methods in custom renderables:
class CustomRenderable extends Renderable {
// Called each frame before rendering
onUpdate(deltaTime: number) {
// Update state, animations, etc.
}
// Called when dimensions change
onResize(width: number, height: number) {
// Respond to size changes
}
// Called when removed from parent
onRemove() {
// Cleanup
}
// Override for custom rendering
renderSelf(buffer: OptimizedBuffer, deltaTime: number) {
// Draw to buffer
}
}
Destroying Renderables
----------------------
Clean up a renderable and remove it from the tree:
// Remove from parent and free resources
renderable.destroy()
// Destroy self and all children
container.destroyRecursively()
---
# Lifecycle and cleanup
Lifecycle and cleanup
=====================
OpenTUI gives you control over terminal cleanup. Call `renderer.destroy()` when you shut down. It restores the terminal to its original state, and it frees resources.
Why you must handle cleanup
---------------------------
OpenTUI does not automatically clean up on `process.exit` or unhandled errors. This design gives you more control over shutdown behavior:
* You may want to handle errors and keep running
* You may use effect systems, like Effect.ts, with their own shutdown handling
* You may need custom cleanup order or additional shutdown logic
Use `renderer.destroy()`
------------------------
Call `destroy()` when your app exits:
import { createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
// ... your application code ...
// Clean shutdown
renderer.destroy()
For reliable cleanup, handle errors and signals in your app:
const renderer = await createCliRenderer()
process.on("uncaughtException", (error) => {
console.error("Uncaught exception:", error)
renderer.destroy()
process.exit(1)
})
process.on("unhandledRejection", (reason) => {
console.error("Unhandled rejection:", reason)
renderer.destroy()
process.exit(1)
})
Signal handling
---------------
OpenTUI listens for common exit signals and calls `destroy()` when it receives them. By default, it handles these signals:
| Signal | Description |
| --- | --- |
| `SIGINT` | Ctrl+C |
| `SIGTERM` | Termination signal |
| `SIGQUIT` | Ctrl+\\ |
| `SIGABRT` | Abort signal |
| `SIGHUP` | Hangup (terminal closed) |
| `SIGBREAK` | Ctrl+Break on Windows |
| `SIGPIPE` | Broken pipe |
| `SIGBUS` | Bus error |
| `SIGFPE` | Floating point exception |
You can customize which signals trigger cleanup:
// Only handle SIGINT and SIGTERM
const renderer = await createCliRenderer({
exitSignals: ["SIGINT", "SIGTERM"],
})
// Disable all signal-based cleanup (handle it yourself)
const renderer = await createCliRenderer({
exitSignals: [],
})
Ctrl+C behavior
---------------
By default, Ctrl+C calls `destroy()`. If you want to handle Ctrl+C yourself, disable the internal handler and remove `SIGINT` from `exitSignals`:
const renderer = await createCliRenderer({
exitOnCtrlC: false,
exitSignals: ["SIGTERM", "SIGQUIT", "SIGABRT", "SIGHUP", "SIGBREAK", "SIGPIPE", "SIGBUS", "SIGFPE"],
})
renderer.keyInput.on("keypress", (key) => {
if (key.ctrl && key.name === "c") {
// Custom Ctrl+C handling
console.log("Ctrl+C pressed, but not exiting")
}
})
Destroy callback
----------------
Run custom logic when the renderer is destroyed:
const renderer = await createCliRenderer({
onDestroy: () => {
console.log("Renderer destroyed, performing additional cleanup...")
},
})
What `destroy()` cleans up
--------------------------
The `destroy()` method cleans up these resources:
* Removes the signal and process listeners that OpenTUI adds
* Clears timers and render loops
* Destroys all renderables in the tree
* Restores stdin raw mode
* Resets terminal state (cursor, alternate screen, etc.)
* Frees native resources
Troubleshooting
---------------
If your terminal stays in a broken state after a crash:
1. Run `reset` in your terminal to restore it
2. Add `uncaughtException` and `unhandledRejection` handlers to your app
3. Make sure you call `renderer.destroy()` in all exit paths
---
# Layout System
Layout System
=============
OpenTUI uses the Yoga layout engine to provide CSS Flexbox-like capabilities for responsive terminal layouts. You can create complex, dynamic interfaces that adapt to terminal size changes.
Flexbox Basics
--------------
The layout system supports standard flexbox properties:
import { BoxRenderable, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const container = new BoxRenderable(renderer, {
id: "container",
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
width: "100%",
height: 10,
})
const leftPanel = new BoxRenderable(renderer, {
id: "left",
flexGrow: 1,
height: 10,
backgroundColor: "#444",
})
const rightPanel = new BoxRenderable(renderer, {
id: "right",
width: 20,
height: 10,
backgroundColor: "#666",
})
container.add(leftPanel)
container.add(rightPanel)
renderer.root.add(container)
Flex Direction
--------------
Control the direction of child elements:
// Vertical layout (default)
{
flexDirection: "column"
}
// Horizontal layout
{
flexDirection: "row"
}
// Reversed directions
{
flexDirection: "row-reverse"
}
{
flexDirection: "column-reverse"
}
Justify Content
---------------
Align children along the main axis:
{
justifyContent: "flex-start"
} // Pack at start
{
justifyContent: "flex-end"
} // Pack at end
{
justifyContent: "center"
} // Center children
{
justifyContent: "space-between"
} // Even spacing, no edge gaps
{
justifyContent: "space-around"
} // Even spacing with edge gaps
{
justifyContent: "space-evenly"
} // Truly even spacing
Align Items
-----------
Align children along the cross axis:
{
alignItems: "flex-start"
} // Align to start
{
alignItems: "flex-end"
} // Align to end
{
alignItems: "center"
} // Center on cross axis
{
alignItems: "stretch"
} // Stretch to fill (default)
{
alignItems: "baseline"
} // Align baselines
Sizing
------
### Fixed Sizes
{
width: 30, // Fixed width in characters
height: 10, // Fixed height in rows
}
### Percentage Sizes
{
width: "100%", // Full width of parent
height: "50%", // Half height of parent
}
### Flex Growing and Shrinking
{
flexGrow: 1, // Take up available space
flexShrink: 0, // Don't shrink below content size
flexBasis: 100, // Initial size before flex adjustments
}
Positioning
-----------
### Relative (Default)
Elements flow normally in the layout:
{
position: "relative",
}
### Absolute
Position elements relative to their parent:
{
position: "absolute",
left: 10,
top: 5,
right: 10,
bottom: 5,
}
Padding and Margin
------------------
{
// Uniform padding
padding: 2,
// Axis-specific
paddingX: 4,
paddingY: 2,
// Individual sides
paddingTop: 1,
paddingRight: 2,
paddingBottom: 1,
paddingLeft: 2,
// Margin works the same way
margin: 1,
marginX: 3,
marginY: 1,
marginTop: 1,
marginRight: 2,
marginBottom: 1,
marginLeft: 2,
}
Using Constructs
----------------
The same layout properties work with the declarative API:
import { Box, Text } from "@opentui/core"
renderer.root.add(
Box(
{
flexDirection: "row",
width: "100%",
height: 10,
},
Box(
{
flexGrow: 1,
backgroundColor: "#333",
padding: 1,
},
Text({ content: "Left Panel" }),
),
Box(
{
width: 20,
backgroundColor: "#555",
padding: 1,
},
Text({ content: "Right Panel" }),
),
),
)
Responsive Layouts
------------------
Listen for terminal resize events to create responsive layouts:
const renderer = await createCliRenderer()
renderer.on("resize", (width, height) => {
// Update layout based on new dimensions
if (width < 80) {
container.flexDirection = "column"
} else {
container.flexDirection = "row"
}
})
---
# Constructs
Constructs
==========
Constructs let you compose your UI in a declarative, React-like way. They are factory functions that create VNodes (virtual nodes). A VNode is a lightweight description of a component. When you add a VNode to the tree, it becomes an actual Renderable.
Basic Usage
-----------
Constructs look like function calls that return component descriptions:
import { createCliRenderer, Box, Text, Input } from "@opentui/core"
const renderer = await createCliRenderer()
renderer.root.add(
Box(
{ width: 40, height: 10, borderStyle: "rounded", padding: 1 },
Text({ content: "Welcome!" }),
Input({ placeholder: "Enter your name..." }),
),
)
Pass children as additional arguments after the props object, not as a property.
Available Constructs
--------------------
Most core Renderable classes have matching construct functions:
import {
ASCIIFont,
Box,
Code,
FrameBuffer,
Input,
ScrollBox,
Select,
SyntaxStyle,
TabSelect,
Text,
RGBA,
} from "@opentui/core"
// These create VNodes, not actual Renderables
const syntaxStyle = SyntaxStyle.fromStyles({
default: { fg: RGBA.fromHex("#FFFFFF") },
})
const box = Box({ border: true })
const text = Text({ content: "Hello" })
const input = Input({ placeholder: "Type here..." })
const code = Code({ content: "const x = 1", filetype: "typescript", syntaxStyle })
const scrollBox = ScrollBox({ width: 40, height: 10 })
const frameBuffer = FrameBuffer({ width: 20, height: 10 })
const ascii = ASCIIFont({ text: "OPEN", font: "tiny" })
How Constructs Work
-------------------
When you call a construct like `Box()`, it creates a VNode - a plain object describing what to create:
// This creates a VNode, not an actual BoxRenderable
const myBox = Box({ width: 20, height: 10 })
// The VNode is instantiated when added to the tree
renderer.root.add(myBox) // Now it becomes a real BoxRenderable
This deferred creation lets you:
* Compose UI without a render context
* Queue method calls before the component exists
* Write cleaner, more declarative code
Creating Custom Constructs
--------------------------
Create reusable components by writing functions that return VNodes:
function LabeledInput(props: { label: string; placeholder: string }) {
return Box(
{ flexDirection: "row", gap: 1 },
Text({ content: props.label }),
Input({ placeholder: props.placeholder, width: 20 }),
)
}
renderer.root.add(
Box(
{ flexDirection: "column", padding: 1 },
LabeledInput({ label: "Name:", placeholder: "Enter name..." }),
LabeledInput({ label: "Email:", placeholder: "Enter email..." }),
),
)
Method Chaining on VNodes
-------------------------
VNodes support method calls. The system queues these calls and applies them after creating the component:
const input = Input({ id: "my-input", placeholder: "Type here..." })
// The VNode queues this call
input.focus()
// When added, the system creates the input and calls focus()
renderer.root.add(input)
You can also set properties:
const box = Box({ id: "my-box" })
box.backgroundColor = RGBA.fromHex("#333366")
renderer.root.add(box)
The delegate() Function
-----------------------
Composite components often need outer method calls to reach a specific inner component. The `delegate()` function maps method and property names to descendant IDs:
import { delegate, Box, Text, Input } from "@opentui/core"
function LabeledInput(props: { id: string; label: string; placeholder: string }) {
return delegate(
{
focus: `${props.id}-input`, // Route focus() to the input
value: `${props.id}-input`, // Route value property access
},
Box(
{ flexDirection: "row" },
Text({ content: props.label }),
Input({
id: `${props.id}-input`,
placeholder: props.placeholder,
width: 20,
}),
),
)
}
const username = LabeledInput({ id: "username", label: "Username:", placeholder: "Enter username..." })
// This actually focuses the nested Input, not the outer Box
username.focus()
renderer.root.add(username)
### Delegate Mappings
The mapping object’s keys are method or property names, and the values are descendant IDs:
delegate(
{
focus: "inner-input", // .focus() -> find descendant "inner-input" and call focus()
blur: "inner-input", // .blur() -> same
add: "content-area", // .add() -> add children to "content-area" instead
value: "inner-input", // .value get/set -> proxy to "inner-input"
},
vnode,
)
Composing with Children
-----------------------
Custom constructs can accept and pass through children:
function Card(props: { title: string }, ...children: VChild[]) {
return Box(
{ border: true, padding: 1, flexDirection: "column" },
Text({ content: props.title, fg: "#FFFF00" }),
Box({ flexDirection: "column" }, ...children),
)
}
renderer.root.add(Card({ title: "User Info" }, Text({ content: "Name: Alice" }), Text({ content: "Role: Admin" })))
Mixing Renderables and Constructs
---------------------------------
You can mix both approaches:
import { BoxRenderable, Text, Input } from "@opentui/core"
// Create a renderable directly
const container = new BoxRenderable(renderer, {
id: "container",
flexDirection: "column",
})
// Add constructs to it
container.add(Text({ content: "Title" }), Input({ placeholder: "Type here..." }))
renderer.root.add(container)
Or use constructs that contain renderables:
const customRenderable = new CustomRenderable(renderer, { id: "custom" })
renderer.root.add(
Box(
{ padding: 1 },
Text({ content: "Header" }),
customRenderable, // Regular renderable mixed in
Text({ content: "Footer" }),
),
)
Next Steps
----------
See [Renderables vs Constructs](https://opentui.com/docs/core-concepts/renderables-vs-constructs)
for a detailed comparison of when to use each approach.
---
# Keyboard input
Keyboard input
==============
OpenTUI parses terminal input and provides structured key events. The `renderer.keyInput` EventEmitter emits `keypress` events plus raw `paste` events with optional metadata.
Basic key handling
------------------
import { createCliRenderer, type KeyEvent } from "@opentui/core"
const renderer = await createCliRenderer()
const keyHandler = renderer.keyInput
keyHandler.on("keypress", (key: KeyEvent) => {
console.log("Key name:", key.name)
console.log("Sequence:", key.sequence)
console.log("Ctrl pressed:", key.ctrl)
console.log("Shift pressed:", key.shift)
console.log("Alt pressed:", key.meta)
console.log("Option pressed:", key.option)
})
KeyEvent properties
-------------------
Each `KeyEvent` contains:
| Property | Type | Description |
| --- | --- | --- |
| `name` | `string` | The key name (e.g., “a”, “escape”, “f1”) |
| `sequence` | `string` | The raw escape sequence |
| `ctrl` | `boolean` | Whether Ctrl was held |
| `shift` | `boolean` | Whether Shift was held |
| `meta` | `boolean` | Whether Alt/Meta was held |
| `option` | `boolean` | Whether Option was held (macOS) |
Common key patterns
-------------------
### Single keys
keyHandler.on("keypress", (key: KeyEvent) => {
if (key.name === "escape") {
console.log("Escape pressed!")
}
if (key.name === "return") {
console.log("Enter pressed!")
}
if (key.name === "space") {
console.log("Space pressed!")
}
})
### Modifier combinations
keyHandler.on("keypress", (key: KeyEvent) => {
// Ctrl+C
if (key.ctrl && key.name === "c") {
console.log("Ctrl+C pressed!")
}
// Ctrl+S
if (key.ctrl && key.name === "s") {
console.log("Save shortcut!")
}
// Shift+F1
if (key.shift && key.name === "f1") {
console.log("Shift+F1 pressed!")
}
// Alt+Enter
if (key.meta && key.name === "return") {
console.log("Alt+Enter pressed!")
}
})
### Function keys
keyHandler.on("keypress", (key: KeyEvent) => {
// F1-F12
if (key.name === "f1") {
showHelp()
}
if (key.name === "f5") {
refresh()
}
})
### Arrow keys
keyHandler.on("keypress", (key: KeyEvent) => {
switch (key.name) {
case "up":
moveCursorUp()
break
case "down":
moveCursorDown()
break
case "left":
moveCursorLeft()
break
case "right":
moveCursorRight()
break
}
})
Paste events
------------
Handle pasted bytes separately from individual keypresses. Decode them explicitly when you expect text:
import { type PasteEvent } from "@opentui/core"
const textDecoder = new TextDecoder()
keyHandler.on("paste", (event: PasteEvent) => {
console.log("Pasted bytes:", event.bytes)
console.log("Decoded text:", textDecoder.decode(event.bytes))
console.log("Metadata:", event.metadata)
})
Exit on Ctrl+C
--------------
Configure the renderer to automatically exit on Ctrl+C:
const renderer = await createCliRenderer({
exitOnCtrlC: true, // Default behavior
})
// Or handle it manually
const renderer = await createCliRenderer({
exitOnCtrlC: false,
})
renderer.keyInput.on("keypress", (key: KeyEvent) => {
if (key.ctrl && key.name === "c") {
// Custom cleanup before exit
cleanup()
process.exit(0)
}
})
Focus and key routing
---------------------
Focus components to receive keyboard input. OpenTUI routes events to the focused component:
import { InputRenderable } from "@opentui/core"
const input = new InputRenderable(renderer, {
id: "my-input",
placeholder: "Type here...",
})
// Focus the input to receive key events
input.focus()
// Or with constructs
import { Input } from "@opentui/core"
const inputNode = Input({ placeholder: "Type here..." })
inputNode.focus() // Queued for when instantiated
renderer.root.add(inputNode)
---
# Renderer
Renderer
========
The `CliRenderer` drives OpenTUI. It manages terminal output, handles input events, runs the rendering loop, and provides context for creating renderables.
Creating a renderer
-------------------
Create a renderer with the async factory function:
import { createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer({
exitOnCtrlC: true,
targetFps: 30,
})
The factory function does three things:
1. Loads the native Zig rendering library
2. Configures terminal settings (mouse, keyboard protocol, and the selected screen mode)
3. Returns an initialized `CliRenderer` instance
Configuration options
---------------------
This page covers the options that most apps set directly. `CliRendererConfig` also includes lower-level hooks for testing and tuning, such as `stdin`, `stdout`, `clock`, `postProcessFns`, and `prependInputHandlers`.
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `screenMode` | `ScreenMode` | `"alternate-screen"` | How the renderer uses terminal space ([details](https://opentui.com/docs/core-concepts/renderer#screen-modes)
) |
| `footerHeight` | `number` | `12` | Requested footer rows for `"split-footer"` mode |
| `externalOutputMode` | `ExternalOutputMode` | varies | How writes to `stdout.write` are handled ([details](https://opentui.com/docs/core-concepts/renderer#external-output-mode)
) |
| `consoleMode` | `ConsoleMode` | `"console-overlay"` | How the built-in console overlay behaves ([details](https://opentui.com/docs/core-concepts/renderer#console-mode)
) |
| `exitOnCtrlC` | `boolean` | `true` | Call `renderer.destroy()` when Ctrl+C is pressed |
| `exitSignals` | `NodeJS.Signals[]` | see below | Signals that trigger cleanup ([details](https://opentui.com/docs/core-concepts/lifecycle#signal-handling)
) |
| `targetFps` | `number` | `30` | Target frames per second for the render loop |
| `maxFps` | `number` | `60` | Maximum FPS cap for immediate re-renders |
| `useMouse` | `boolean` | `true` | Enable mouse input |
| `autoFocus` | `boolean` | `true` | Focus the nearest focusable renderable on left click |
| `enableMouseMovement` | `boolean` | `true` | Track mouse movement, not just clicks and scrolls |
| `useKittyKeyboard` | `KittyKeyboardOptions \| null` | `{}` | Kitty keyboard protocol settings, or `null` to disable |
| `backgroundColor` | `ColorInput` | transparent | Background color for the render buffer |
| `consoleOptions` | `ConsoleOptions` | \- | Options forwarded to the built-in console overlay |
| `openConsoleOnError` | `boolean` | `true` (dev) | Open the console overlay on uncaught errors |
| `onDestroy` | `() => void` | \- | Run after the renderer finishes cleanup |
You can also change `screenMode`, `footerHeight`, `externalOutputMode`, and `consoleMode` at runtime through their matching `renderer` properties.
Screen modes
------------
The `screenMode` option controls whether OpenTUI owns the alternate screen or a reserved region of the main screen. You can also change modes at runtime by setting `renderer.screenMode`.
### `"alternate-screen"` (default)
Switches to the terminal’s alternate screen buffer. The original scrollback content is preserved and restored when the renderer exits. This is the standard mode for full-screen TUI applications.
const renderer = await createCliRenderer({
screenMode: "alternate-screen",
})
### `"main-screen"`
Renders on the terminal’s main screen without switching buffers. OpenTUI still reserves a render region by scrolling terminal content, so this is not a true scrollback-native inline or direct renderer. Use it when you want the UI to stay on the main screen for testing, benchmarks, or short-lived tools.
const renderer = await createCliRenderer({
screenMode: "main-screen",
})
### `"split-footer"`
Pins the renderer to a reserved footer region at the bottom of the terminal. The area above the footer stays available for normal program output. This is the closest thing OpenTUI currently has to a direct-render mode, but it still uses the same buffered main-screen renderer rather than a separate inline backend.
The `footerHeight` option controls how many rows the footer requests (default: 12). When using split-footer mode, `externalOutputMode` defaults to `"capture-stdout"` so that writes to `stdout.write` can be replayed above the footer instead of overlapping it.
const renderer = await createCliRenderer({
screenMode: "split-footer",
footerHeight: 20,
})
// Change footer height at runtime
renderer.footerHeight = 15
External output mode
--------------------
The `externalOutputMode` option controls what happens to writes that go through the renderer’s configured `stdout.write` while the renderer is active. It does not change `stderr`, and it is separate from the built-in console overlay.
The native renderer still paints to the real TTY. `externalOutputMode` only changes the TypeScript-side `stdout.write` path.
* **`"capture-stdout"`**: Intercepts `stdout.write`, queues the text, and flushes it above the footer during split-footer renders. Only valid when `screenMode` is `"split-footer"`.
* **`"passthrough"`**: Leaves `stdout.write` untouched. Output goes directly to the terminal.
The default depends on the screen mode: `"capture-stdout"` for split-footer, `"passthrough"` for everything else.
// Captured stdout appears above the footer
const renderer = await createCliRenderer({
screenMode: "split-footer",
externalOutputMode: "capture-stdout",
})
// Switch at runtime
renderer.externalOutputMode = "passthrough"
Console mode
------------
The `consoleMode` option controls the built-in console overlay (`TerminalConsole`).
* **`"console-overlay"`** (default): Captures console output (console.log, console.error, etc.) and renders it in a toggleable panel within the TUI.
* **`"disabled"`**: Hides and deactivates the overlay surface.
Current caveat: `consoleMode` only changes the overlay surface. If you need plain `console.*` behavior, set `OTUI_USE_CONSOLE=false`.
const renderer = await createCliRenderer({
screenMode: "split-footer",
externalOutputMode: "capture-stdout",
consoleMode: "disabled",
})
The root renderable
-------------------
Every renderer has a `root` property. It is a special `RootRenderable` at the top of the component tree:
import { Box, Text } from "@opentui/core"
// Add components to the root
renderer.root.add(Box({ width: 40, height: 10, borderStyle: "rounded" }, Text({ content: "Hello, OpenTUI!" })))
The root renderable fills the entire terminal and adjusts when you resize it.
Render loop control
-------------------
You can use these control modes:
### Automatic mode (default)
If you do not call `start()`, the renderer re-renders only when the component tree changes:
const renderer = await createCliRenderer()
renderer.root.add(Text({ content: "Static content" })) // Triggers render
### Continuous mode
Call `start()` to run the render loop continuously at the target FPS:
renderer.start() // Start continuous rendering
renderer.stop() // Stop the render loop
### Live rendering
For animations, call `requestLive()` to enable continuous rendering:
// Request live mode (increments internal counter)
renderer.requestLive()
// When animation completes, drop the request
renderer.dropLive()
Multiple components can request animations at the same time. The renderer stays live until all requests drop.
### Pause and suspend
renderer.pause() // Pause rendering (use start() or requestLive() to run it again)
renderer.suspend() // Fully suspend (disables mouse, input, and raw mode)
renderer.resume() // Resume from suspended state
Key properties
--------------
| Property | Type | Description |
| --- | --- | --- |
| `root` | `RootRenderable` | Root of the component tree |
| `width` | `number` | Current render width in columns |
| `height` | `number` | Current render height in rows |
| `console` | `TerminalConsole` | Built-in console overlay |
| `keyInput` | `KeyHandler` | Keyboard input handler |
| `isRunning` | `boolean` | Whether the render loop is active |
| `isDestroyed` | `boolean` | Whether the renderer has been destroyed |
| `currentFocusedRenderable` | `Renderable \| null` | Currently focused component |
Events
------
Use `renderer.on(event, callback)` to subscribe:
| Event | Payload | Description |
| --- | --- | --- |
| `resize` | `(width: number, height: number)` | Terminal window resized |
| `focus` | `()` | Terminal window gained focus |
| `blur` | `()` | Terminal window lost focus |
| `theme_mode` | `(mode: "dark" \| "light")` | Terminal color scheme changed. See [Theme mode](https://opentui.com/docs/core-concepts/renderer#theme-mode)
section. |
| `capabilities` | `(caps: Capabilities)` | Terminal capabilities detected |
| `selection` | `(selection: Selection)` | Text selection completed |
| `destroy` | `()` | Renderer destroyed |
| `memory:snapshot` | `(snapshot: MemorySnapshot)` | Memory usage snapshot available |
| `debugOverlay:toggle` | `(enabled: boolean)` | Debug overlay visibility changed |
// Terminal resized
renderer.on("resize", (width, height) => {
console.log(`Terminal size: ${width}x${height}`)
})
// Terminal focus events
renderer.on("focus", () => {
console.log("Terminal gained focus")
})
renderer.on("blur", () => {
console.log("Terminal lost focus")
})
// Renderer destroyed
renderer.on("destroy", () => {
console.log("Renderer destroyed")
})
// Text selection completed
renderer.on("selection", (selection) => {
console.log("Selected text:", selection.getSelectedText())
})
Theme mode
----------
OpenTUI can detect the terminal’s preferred color scheme (dark or light) when the terminal supports DEC mode 2031 color scheme updates. Read the current mode via `renderer.themeMode` and subscribe to `theme_mode` to react to changes. Possible values are `"dark"`, `"light"`, or `null` when unsupported, and no events fire in the unsupported case.
import { type ThemeMode } from "@opentui/core"
const mode = renderer.themeMode
renderer.on("theme_mode", (nextMode: ThemeMode) => {
console.log("Theme mode changed:", nextMode)
})
Cursor control
--------------
Use these methods to control the cursor position and style:
// Position and visibility
renderer.setCursorPosition(10, 5, true)
// Cursor style
//
// Available styles: "default", "block", "underline", "line"
// The default style is "default", which preserves the terminal's native cursor
// style instead of overriding it.
renderer.setCursorStyle({ style: "block", blinking: true }) // Blinking block
renderer.setCursorStyle({ style: "underline", blinking: false }) // Steady underline
renderer.setCursorStyle({ style: "line", blinking: true }) // Blinking line
renderer.setCursorStyle({ style: "default" }) // Reset to terminal's native cursor
// Cursor style with color
renderer.setCursorStyle({
style: "block",
blinking: true,
color: RGBA.fromHex("#FF0000"),
})
// Cursor style with mouse pointer
//
// Types of mouse pointers available: "default", "pointer", "text", "crosshair", "move",
// "not-allowed"
renderer.setCursorStyle({
style: "block",
blinking: false,
cursor: "pointer",
})
Input handling
--------------
Add custom input handlers:
renderer.addInputHandler((sequence) => {
if (sequence === "\x1b[A") {\
// Up arrow - handle and consume\
return true\
}\
return false // Let other handlers process\
})\
\
By default, `addInputHandler()` appends handlers to the chain and runs them after built-in handlers. Use `prependInputHandler()` to add a handler at the start of the chain and run it before built-in handlers.\
\
Debug overlay\
-------------\
\
Use the debug overlay to show FPS, memory usage, and other stats:\
\
renderer.toggleDebugOverlay()\
\
// You can also configure it\
import { DebugOverlayCorner } from "@opentui/core"\
\
renderer.configureDebugOverlay({\
enabled: true,\
corner: DebugOverlayCorner.topRight,\
})\
\
Cleanup\
-------\
\
Always destroy the renderer when you finish so you restore the terminal state:\
\
renderer.destroy()\
\
Destroying the renderer restores the terminal to its original state, disables mouse tracking, and cleans up resources.\
\
**Important:** OpenTUI does not automatically clean up on `process.exit` or unhandled errors. This design gives you control. See [Lifecycle](https://opentui.com/docs/core-concepts/lifecycle/)\
for signal handling options and best practices.\
\
Environment variables\
---------------------\
\
See the [environment variable reference](https://opentui.com/docs/reference/env-vars)\
for the full list.\
\
The renderer-specific ones you will most likely care about are:\
\
* `OTUI_USE_CONSOLE` controls global `console.*` capture.\
* `SHOW_CONSOLE` opens the built-in console overlay at startup.\
* `OTUI_NO_NATIVE_RENDER` skips the Zig/native frame renderer. In `"split-footer"` mode, the current stdout flush path can still write ANSI.\
* `OTUI_DUMP_CAPTURES` dumps captured stdout and console caches from the renderer exit handler.
---
# Console overlay
Console overlay
===============
OpenTUI includes a built-in console overlay that captures all `console.log`, `console.info`, `console.warn`, `console.error`, and `console.debug` calls. You can position the console at any edge of the terminal. It supports scrolling and focus management.
Basic usage
-----------
import { createCliRenderer, ConsolePosition } from "@opentui/core"
const renderer = await createCliRenderer({
consoleOptions: {
position: ConsolePosition.BOTTOM,
sizePercent: 30,
},
})
// These appear in the overlay instead of stdout
console.log("This appears in the overlay")
console.error("Errors are color-coded red")
console.warn("Warnings appear in yellow")
Configuration options
---------------------
const renderer = await createCliRenderer({
consoleOptions: {
position: ConsolePosition.BOTTOM, // Position on screen
sizePercent: 30, // Size as percentage of terminal
colorInfo: "#00FFFF", // Color for console.info
colorWarn: "#FFFF00", // Color for console.warn
colorError: "#FF0000", // Color for console.error
startInDebugMode: false, // Show file/line info in logs
},
})
Console positions
-----------------
import { ConsolePosition } from "@opentui/core"
ConsolePosition.TOP // Dock at top
ConsolePosition.BOTTOM // Dock at bottom
ConsolePosition.LEFT // Dock at left
ConsolePosition.RIGHT // Dock at right
Toggling the console
--------------------
Toggle the console overlay in code:
// Toggle visibility and focus
renderer.console.toggle()
// When open but not focused, toggle() focuses the console
// When focused, toggle() closes the console
Console shortcuts
-----------------
When the console is focused:
| Key | Action |
| --- | --- |
| Arrow keys | Scroll through log history |
| `+` | Increase console size |
| `-` | Decrease console size |
Keybinding for toggle
---------------------
Add a keyboard shortcut to toggle the console:
renderer.keyInput.on("keypress", (key) => {
// Toggle with backtick key
if (key.name === "`") {
renderer.console.toggle()
}
// Or with a modifier
if (key.ctrl && key.name === "l") {
renderer.console.toggle()
}
})
Why use the console?
--------------------
Terminal UI applications capture stdout for rendering. Regular `console.log` calls would interfere with your interface. The console overlay solves this problem:
* Captures all console output without disrupting the UI
* Displays logs in a dedicated overlay area
* Color-codes different log levels for easy identification
* Lets you scroll through history for debugging
Environment variables
---------------------
Control console behavior with environment variables:
# Disable console capture entirely
OTUI_USE_CONSOLE=false bun app.ts
# Start with console visible
SHOW_CONSOLE=true bun app.ts
# Dump captured output on exit
OTUI_DUMP_CAPTURES=true bun app.ts
---
# Colors
Colors
======
OpenTUI uses the `RGBA` class for color representation. The class stores colors as normalized float values (0.0-1.0) internally, but provides methods for working with different color formats.
Creating colors
---------------
### From integers (0-255)
import { RGBA } from "@opentui/core"
const red = RGBA.fromInts(255, 0, 0, 255)
const semiTransparentBlue = RGBA.fromInts(0, 0, 255, 128)
### From float values (0.0-1.0)
const green = RGBA.fromValues(0.0, 1.0, 0.0, 1.0)
const transparent = RGBA.fromValues(1.0, 1.0, 1.0, 0.5)
### From hex strings
const purple = RGBA.fromHex("#800080")
const withAlpha = RGBA.fromHex("#FF000080") // Semi-transparent red
String colors
-------------
Most component properties accept both `RGBA` objects and color strings:
import { Text, Box } from "@opentui/core"
// Using hex strings
Text({ content: "Hello", fg: "#00FF00" })
// Using CSS color names
Box({ backgroundColor: "red", borderColor: "white" })
// Using RGBA objects
const customColor = RGBA.fromInts(100, 150, 200, 255)
Text({ content: "Custom", fg: customColor })
// Transparent
Box({ backgroundColor: "transparent" })
The parseColor utility
----------------------
The `parseColor()` function converts various color formats to RGBA:
import { parseColor } from "@opentui/core"
const color1 = parseColor("#FF0000") // Hex
const color2 = parseColor("blue") // CSS color name
const color3 = parseColor("transparent") // Transparent
const color4 = parseColor(RGBA.fromInts(255, 0, 0, 255)) // Pass-through
Alpha blending
--------------
You can use transparent cells and alpha blending for layered effects:
import { FrameBufferRenderable, RGBA } from "@opentui/core"
const canvas = new FrameBufferRenderable(renderer, {
id: "canvas",
width: 50,
height: 20,
})
// Draw with alpha blending
const semiTransparent = RGBA.fromValues(1.0, 0.0, 0.0, 0.5)
const transparent = RGBA.fromInts(0, 0, 0, 0)
canvas.frameBuffer.setCellWithAlphaBlending(10, 5, " ", transparent, semiTransparent)
Text attributes with colors
---------------------------
Combine colors with text attributes:
import { TextRenderable, TextAttributes, RGBA } from "@opentui/core"
const styledText = new TextRenderable(renderer, {
id: "styled",
content: "Important",
fg: RGBA.fromHex("#FFFF00"),
bg: RGBA.fromHex("#333333"),
attributes: TextAttributes.BOLD | TextAttributes.UNDERLINE,
})
Color constants
---------------
Common colors:
// Some examples of commonly used colors
const white = RGBA.fromInts(255, 255, 255, 255)
const black = RGBA.fromInts(0, 0, 0, 255)
const red = RGBA.fromInts(255, 0, 0, 255)
const green = RGBA.fromInts(0, 255, 0, 255)
const blue = RGBA.fromInts(0, 0, 255, 255)
const transparent = RGBA.fromInts(0, 0, 0, 0)
---
# Plugin Slots
Plugin slots
============
Most TUI apps start as one codebase. As features and teams grow, you may want extension points without asking users to fork the app.
Plugin slots give you that. The host application defines named places in the layout (for example, a status bar, sidebar, or panel), and plugins contribute UI for those places at runtime.
The host stays in control of layout, rendering mode, and type contracts. Plugins only receive the context and slot props the host intentionally exposes.
This page describes the **shared registry API** that the core, React, and Solid bindings are built on. If you are building an app, start with the page for the binding you use:
* [Core slots](https://opentui.com/docs/plugins/core)
— framework-free, `BaseRenderable` nodes
* [React slots](https://opentui.com/docs/plugins/react)
— `ReactNode`
* [Solid slots](https://opentui.com/docs/plugins/solid)
— `JSX.Element`
If you load plugin modules from disk at runtime, see the runtime support notes on the [React slots page](https://opentui.com/docs/plugins/react#runtime-loaded-external-plugins)
and [Solid slots page](https://opentui.com/docs/plugins/solid#runtime-loaded-external-plugins)
. For custom hosts, use `@opentui/core/runtime-plugin-support` or `createRuntimePlugin` from `@opentui/core/runtime-plugin`.
Read on if you need to understand the underlying model, want to build a custom binding for another framework, or need direct access to the registry API.
Concepts
--------
* **Host**: defines slot names and slot prop types.
* **Plugin**: contributes one or more slot renderer callbacks. May include lifecycle hooks.
* **Registry**: registers plugins and resolves contributions for a slot.
* **Slot mode**: controls how plugin output and fallback UI combine (`append`, `replace`, or `single_winner`).
Define slots and host context
-----------------------------
Slot names map to the props each slot receives. The host context is a shared object passed to every plugin renderer.
import type { PluginContext } from "@opentui/core"
type AppSlots = {
statusbar: { user: string }
sidebar: { section: "left" | "right" }
}
interface AppContext extends PluginContext {
appName: string
version: string
}
The context can be any object. `PluginContext` is a type alias for `object` — extending it is not required but gives your plugins a named type to reference.
Create a registry
-----------------
import { createSlotRegistry } from "@opentui/core"
const context = { appName: "my-app", version: "1.0.0" }
const registry = createSlotRegistry(renderer, "my-app:plugins", context)
The first type parameter (`TNode`) is the node type your framework returns — `BaseRenderable` for core, `ReactNode` for React, `JSX.Element` for Solid. The framework-specific helpers fill this in for you.
`createSlotRegistry` is renderer-scoped. For a given `(renderer, key)` pair, you always get the same registry instance. The `key` parameter namespaces registries within a renderer so multiple independent registries can coexist.
Calling `createSlotRegistry` again with the same `(renderer, key)` pair returns the existing registry and applies the new `options` via `configure()`. The `context` argument **must** be the same object reference — passing a different context object throws an error. This means you should create the context object once and reuse it:
// correct — same object reference
const context = { appName: "my-app" }
const reg1 = createSlotRegistry(renderer, "my-key", context)
const reg2 = createSlotRegistry(renderer, "my-key", context) // returns reg1
// throws — different object even if structurally identical
const reg3 = createSlotRegistry(renderer, "my-key", { appName: "my-app" })
When the renderer is destroyed, all registries scoped to it are automatically cleared and disposed.
The framework-specific helpers (`createCoreSlotRegistry`, `createReactSlotRegistry`, `createSolidSlotRegistry`) call `createSlotRegistry` internally with a fixed key. Use `createSlotRegistry` directly only if you need multiple independent registries per renderer.
### Registry options
All `create*SlotRegistry` functions accept an optional `SlotRegistryOptions` object:
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `onPluginError` | `(event: PluginErrorEvent) => void` | — | Callback invoked on every plugin error |
| `debugPluginErrors` | `boolean` | `false` | When `true`, errors are also logged via `console.debug` |
| `maxPluginErrors` | `number` | `100` | Maximum number of buffered errors before oldest are dropped |
Register plugins
----------------
const unregister = registry.register({
id: "clock-plugin",
order: 0,
setup(ctx, renderer) {
// called once on registration — initialize resources here
},
dispose() {
// called when the plugin is unregistered — clean up resources here
},
slots: {
statusbar(ctx, props) {
return `${ctx.appName}:${props.user}`
},
},
})
// later: remove this plugin
unregister()
`register()` returns an unregister function. Calling it removes the plugin and invokes its `dispose` hook.
### Plugin interface
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `string` | yes | Unique identifier. Duplicate ids throw. |
| `order` | `number` | no | Sort priority (ascending). Defaults to `0`. |
| `setup` | `(ctx, renderer) => void` | no | Called once at registration time. If it throws, the plugin is not registered. |
| `dispose` | `() => void` | no | Called when the plugin is unregistered or the registry is cleared. |
| `slots` | `{ [slotName]: (ctx, props) => TNode }` | yes | Slot renderer callbacks. Each receives the host context and slot-specific props. |
The core binding extends this with [managed slot objects](https://opentui.com/docs/plugins/core#managed-slot-contributions)
that add lifecycle hooks.
### Ordering
Plugins are resolved in this order:
1. `order` ascending (lower numbers first)
2. Registration order (earlier registrations first)
3. `id` lexicographic (tie-breaker)
Slot modes
----------
Every slot mount or `` component accepts a `mode`. The mode controls how plugin output and fallback UI combine.
| Mode | Behavior |
| --- | --- |
| `append` | Fallback first, then all plugin output (default) |
| `replace` | Plugin output only; fallback shown only when no plugins produce output |
| `single_winner` | Only the first plugin by resolved order; fallback if it produces no output |
Resolve contributions
---------------------
const entries = registry.resolveEntries("statusbar")
// Array<{ id: string, renderer: (ctx, props) => TNode }>
const slotRenderers = registry.resolve("statusbar")
// Array<(ctx, props) => TNode>
`renderer` here means the plugin’s **slot renderer callback**, not the `CliRenderer` instance.
Use `resolveEntries` when you need plugin ids alongside the callbacks. Use `resolve` when you only need the callbacks.
Registry methods
----------------
| Method | Description |
| --- | --- |
| `register(plugin)` | Add a plugin. Returns an unregister function. |
| `unregister(id)` | Remove a plugin by id. Returns `true` if found. |
| `updateOrder(id, order)` | Change a plugin’s sort order. Returns `true` if found. |
| `clear()` | Remove and dispose all plugins. |
| `resolve(slot)` | Return ordered slot renderer callbacks. |
| `resolveEntries(slot)` | Return ordered `{ id, renderer }` entries. |
| `subscribe(listener)` | Listen for registration changes. Returns an unsubscribe function. Used internally by React and Solid to trigger re-renders. |
| `configure(options)` | Update `SlotRegistryOptions` after creation. |
| `onPluginError(listener)` | Listen for plugin errors. Returns an unsubscribe function. |
| `getPluginErrors()` | Return buffered `PluginErrorEvent` array. |
| `clearPluginErrors()` | Clear the error buffer. |
| `reportPluginError(report)` | Manually report a plugin error. Used by framework integrations. |
| `renderer` | Getter — the `CliRenderer` this registry is scoped to. |
| `context` | Getter — the host context object. |
Error handling
--------------
Registries expose plugin error events:
registry.onPluginError((event) => {
console.error(event.pluginId, event.phase, event.source, event.error.message)
})
You can also read and clear buffered errors:
const history = registry.getPluginErrors()
registry.clearPluginErrors()
### PluginErrorReport
The `reportPluginError` method accepts a `PluginErrorReport`:
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `pluginId` | `string` | yes | The plugin that caused the error |
| `slot` | `string \| undefined` | no | The slot name, if the error is slot-specific |
| `phase` | `PluginErrorPhase` | yes | `"setup"`, `"render"`, `"dispose"`, or `"error_placeholder"` |
| `source` | `PluginErrorSource` | no | Defaults to `"registry"` if omitted |
| `error` | `unknown` | yes | The raw error — normalized to `Error` internally |
### PluginErrorEvent
| Field | Type | Description |
| --- | --- | --- |
| `pluginId` | `string` | The plugin that caused the error |
| `slot` | `string \| undefined` | The slot name, if the error is slot-specific |
| `phase` | `PluginErrorPhase` | `"setup"`, `"render"`, `"dispose"`, or `"error_placeholder"` |
| `source` | `PluginErrorSource` | `"registry"`, `"core"`, or a framework-defined source string |
| `error` | `Error` | The normalized error object |
| `timestamp` | `number` | `Date.now()` at the time of the error |
Next: concrete host APIs
------------------------
Use the shared model above with one of these host integrations:
* [Core slots](https://opentui.com/docs/plugins/core)
— framework-free, returns `BaseRenderable` nodes
* [React slots](https://opentui.com/docs/plugins/react)
— returns `ReactNode`
* [Solid slots](https://opentui.com/docs/plugins/solid)
— returns `JSX.Element`
---
# React
React slots
===========
This page shows React integration for plugin slots.
Use slots when you want external modules to contribute `ReactNode` UI in host-defined regions without forking your app. The host keeps ownership of layout and slot typing; plugins only see the context and props you expose.
If you have not read the shared model yet, start with [Plugin Slots](https://opentui.com/docs/plugins/slots)
.
Runtime-loaded external plugins
-------------------------------
If your app loads plugins from disk at runtime (for example `await import(fileUrl)`), add this import once in your app entry:
import "@opentui/react/runtime-plugin-support"
This installs Bun runtime support so external TS/TSX plugin modules resolve against the host runtime instances (`@opentui/react`, React JSX runtime modules, and core runtime modules).
Use this for plugin systems in both normal Bun runs and standalone compiled executables.
import "@opentui/react/runtime-plugin-support"
const mod = await import(pathToFileURL(pluginPath).href)
registry.register(mod.loadExternalPlugin())
What React adds
---------------
* `createReactSlotRegistry(renderer, context, options?)` — create a registry typed for `ReactNode`. Accepts the same [`SlotRegistryOptions`](https://opentui.com/docs/plugins/slots#registry-options)
as `createSlotRegistry`.
* `Slot` — generic slot component that takes `registry` as a required prop
* `createSlot(registry, options?)` — optional convenience helper that returns a registry-bound `` component
* `ReactPlugin` — convenience type alias for a plugin that returns `ReactNode`
* `@opentui/react/runtime-plugin-support` — one-line runtime support for external plugin/module loading
Register plugins directly with `registry.register()` — no wrapper function is needed (unlike core’s `registerCorePlugin`).
Basic usage
-----------
import { createCliRenderer } from "@opentui/core"
import { createReactSlotRegistry, createRoot, Slot } from "@opentui/react"
type Slots = {
statusbar: { user: string }
}
const context = { appName: "react-app", version: "1.0.0" }
const renderer = await createCliRenderer()
const registry = createReactSlotRegistry(renderer, context)
const unregister = registry.register({
id: "clock-plugin",
slots: {
statusbar(ctx, props) {
return {`${ctx.appName}:${props.user}`}
},
},
})
const AppSlot = Slot
function App() {
return (
fallback-statusbar
)
}
createRoot(renderer).render()
Optional convenience helper
---------------------------
If you prefer not to pass `registry` each time, you can still bind one once:
const AppSlot = createSlot(registry)
`` props
--------------
| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `registry` | `SlotRegistry` | yes | Registry to resolve plugins from |
| `name` | `keyof Slots` | yes | Which slot to render |
| `mode` | `SlotMode` | no | `"append"` (default), `"replace"`, or `"single_winner"`. See [slot modes](https://opentui.com/docs/plugins/slots#slot-modes)
. |
| `pluginFailurePlaceholder` | `(failure: PluginErrorEvent) => ReactNode` | no | Per-slot placeholder UI when a plugin throws |
| `children` | `ReactNode` | no | Fallback UI |
| _remaining_ | `Slots[name]` | — | Slot-specific props forwarded to plugin renderers |
`ReactSlotOptions` (for `createSlot`)
-------------------------------------
| Option | Type | Required | Description |
| --- | --- | --- | --- |
| `pluginFailurePlaceholder` | `(failure: PluginErrorEvent) => ReactNode` | no | Creates placeholder UI when a plugin throws |
Plugin failure placeholders
---------------------------
const Slot = createSlot(registry, {
pluginFailurePlaceholder(failure) {
return {`plugin-error:${failure.pluginId}:${failure.phase}`}
},
})
If a plugin throws, the slot renders the placeholder. If no placeholder is provided (or it returns `null`), the slot falls back to `children`.
Plugins that throw during the initial render call are caught inline. Plugins that throw during a React re-render are caught by an internal error boundary that resets on registry changes.
Observability
-------------
registry.onPluginError((event) => {
console.error(event.pluginId, event.phase, event.source, event.error.message)
})
Example: `packages/react/examples/plugin-slots-errors.tsx`
---
# Text
Text
====
Display styled text content with support for colors, attributes, and text selection.
Basic Usage
-----------
### Renderable API
import { TextRenderable, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const text = new TextRenderable(renderer, {
id: "greeting",
content: "Hello, OpenTUI!",
fg: "#00FF00",
})
renderer.root.add(text)
### Construct API
import { Text, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
renderer.root.add(
Text({
content: "Hello, OpenTUI!",
fg: "#00FF00",
}),
)
Text attributes
---------------
Combine multiple text attributes using bitwise OR:
import { TextRenderable, TextAttributes } from "@opentui/core"
const styledText = new TextRenderable(renderer, {
id: "styled",
content: "Important Message",
fg: "#FFFF00",
attributes: TextAttributes.BOLD | TextAttributes.UNDERLINE,
})
### Available attributes
| Attribute | Description |
| --- | --- |
| `TextAttributes.BOLD` | Bold text |
| `TextAttributes.DIM` | Dimmed text |
| `TextAttributes.ITALIC` | Italic text |
| `TextAttributes.UNDERLINE` | Underlined text |
| `TextAttributes.BLINK` | Blinking text |
| `TextAttributes.INVERSE` | Inverted colors |
| `TextAttributes.HIDDEN` | Hidden text |
| `TextAttributes.STRIKETHROUGH` | Strikethrough text |
Template literals for rich text
-------------------------------
Use the `t` template literal for inline styling within a single text element:
import { TextRenderable, t, bold, underline, fg, bg, italic } from "@opentui/core"
const richText = new TextRenderable(renderer, {
id: "rich",
content: t`${bold("Important:")} ${fg("#FF0000")(underline("Warning!"))} Normal text`,
})
### Available style functions
import { t, bold, dim, italic, underline, blink, reverse, strikethrough, fg, bg } from "@opentui/core"
// Basic attributes
t`${bold("bold text")}`
t`${italic("italic text")}`
t`${underline("underlined")}`
t`${strikethrough("deleted")}`
// Colors
t`${fg("#FF0000")("red text")}`
t`${bg("#0000FF")("blue background")}`
// Combining styles
t`${bold(fg("#FFFF00")("bold yellow"))}`
Positioning
-----------
const text = new TextRenderable(renderer, {
id: "positioned",
content: "Absolute position",
position: "absolute",
left: 10,
top: 5,
})
Text selection
--------------
Enable text selection for copying:
const selectableText = new TextRenderable(renderer, {
id: "selectable",
content: "Select me!",
selectable: true, // Default is true
})
const nonSelectable = new TextRenderable(renderer, {
id: "label",
content: "Button Label",
selectable: false, // Disable selection
})
Properties
----------
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `content` | `string \| StyledText` | `""` | The text content to display |
| `fg` | `string \| RGBA` | \- | Foreground (text) color |
| `bg` | `string \| RGBA` | \- | Background color |
| `attributes` | `TextAttributes` | `0` | Text styling attributes |
| `selectable` | `boolean` | `true` | Whether text can be selected |
| `position` | `"relative" \| "absolute"` | `"relative"` | Positioning mode |
| `left`, `top`, `right`, `bottom` | `number \| "auto" \| "{number}%"` | \- | Position offsets |
Example: status bar
-------------------
import { Text, Box, t, bold, fg } from "@opentui/core"
const statusBar = Box(
{
position: "absolute",
bottom: 0,
width: "100%",
height: 1,
backgroundColor: "#333333",
flexDirection: "row",
justifyContent: "space-between",
paddingLeft: 1,
paddingRight: 1,
},
Text({
content: t`${bold("myfile.ts")} - ${fg("#888888")("TypeScript")}`,
}),
Text({
content: t`Ln ${fg("#00FF00")("42")}, Col ${fg("#00FF00")("15")}`,
}),
)
renderer.root.add(statusBar)
---
# Core
Core slots
==========
This page shows the core host API for plugin slots.
Use slots when you want external modules to render `BaseRenderable` UI in host-defined layout regions without forking the app. The host keeps control of layout and slot typing; plugins only render through the APIs you expose.
If you have not read the shared model yet, start with [Plugin Slots](https://opentui.com/docs/plugins/slots)
.
What core adds
--------------
On top of `createSlotRegistry`, core provides:
* `createCoreSlotRegistry` — create a registry typed for `BaseRenderable` nodes
* `registerCorePlugin` — register a plugin using the core-specific `CorePlugin` interface
* `SlotRenderable` — a `Renderable` that mounts a slot into the renderable tree
* `resolveCoreSlot` — resolve slot entries without mounting
* `@opentui/core/runtime-plugin-support` / `createRuntimePlugin` — runtime module support for external plugin/module loading in Bun
Core slot renderers receive both the host context and slot data: `(ctx, data) => BaseRenderable`.
`createCoreSlotRegistry` accepts the same [`SlotRegistryOptions`](https://opentui.com/docs/plugins/slots#registry-options)
as `createSlotRegistry`.
Runtime-loaded external plugins
-------------------------------
If your app loads plugin modules from disk at runtime, import this once in your app entry:
import "@opentui/core/runtime-plugin-support"
This installs Bun runtime support for `@opentui/core` plus default core runtime entrypoints (`@opentui/core/3d`, `@opentui/core/testing`).
If you need additional host-resolved modules, register your own plugin:
import { plugin } from "bun"
import { createRuntimePlugin } from "@opentui/core/runtime-plugin"
plugin(
createRuntimePlugin({
additional: {
"my-runtime-module": async () => (await import("./runtime-module")) as Record,
},
}),
)
`CorePlugin` interface
----------------------
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `string` | yes | Unique identifier. Duplicate ids throw. |
| `order` | `number` | no | Sort priority (ascending). Defaults to `0`. |
| `setup` | `(ctx, renderer) => void` | no | Called once at registration time. If it throws, the plugin is not registered. |
| `dispose` | `() => void` | no | Called when the plugin is unregistered or the registry is cleared. |
| `slots` | `Partial>` | yes | Each value is a renderer function or a [managed slot object](https://opentui.com/docs/plugins/core#managed-slot-contributions)
. |
A `CoreSlotContribution` is either a plain renderer `(ctx, data) => BaseRenderable` or a `CoreManagedSlot` with lifecycle hooks.
Basic usage
-----------
A `SlotRenderable` extends `Renderable`, so it can be added to any parent like a `BoxRenderable`. It resolves plugins from the registry, manages their lifecycle, and reconciles their output as its children.
import {
BoxRenderable,
createCliRenderer,
createCoreSlotRegistry,
registerCorePlugin,
SlotRenderable,
TextRenderable,
} from "@opentui/core"
type Slots = "statusbar"
type SlotData = { label: string }
const context = { appName: "core-app", version: "1.0.0" }
const renderer = await createCliRenderer()
const registry = createCoreSlotRegistry(renderer, context)
const unregister = registerCorePlugin(registry, {
id: "clock-plugin",
order: 0,
slots: {
statusbar(_ctx, data) {
return new TextRenderable(renderer, {
id: "clock-status",
content: `clock: ${data.label}`,
})
},
},
})
const slot = new SlotRenderable(renderer, {
id: "statusbar-slot",
registry,
name: "statusbar",
data: { label: "ok" },
mode: "append",
width: "100%",
height: 3,
flexDirection: "row",
fallback: () =>
new TextRenderable(renderer, {
id: "statusbar-fallback",
content: "fallback",
}),
})
renderer.root.add(slot)
// later
slot.mode = "replace"
slot.data = { label: "updated" }
slot.refresh()
slot.destroy()
`registerCorePlugin` returns an unregister function (`() => void`). Calling it removes the plugin and invokes its `dispose` hook.
Because `SlotRenderable` extends `Renderable`, it accepts all standard layout options (`width`, `height`, `flexDirection`, `padding`, etc.) alongside the slot-specific options.
`SlotRenderable` options
------------------------
| Option | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `string` | yes | Unique renderable identifier (inherited from `RenderableOptions`) |
| `registry` | `CoreSlotRegistry` | yes | The registry to read plugins from |
| `name` | slot name | yes | Which slot to mount |
| `data` | object | no | Slot data passed to plugin renderers as the second argument |
| `mode` | `SlotMode` | no | `"append"` (default), `"replace"`, or `"single_winner"`. See [slot modes](https://opentui.com/docs/plugins/slots#slot-modes)
. |
| `fallback` | `BaseRenderable \| BaseRenderable[] \| () => ...` | no | Fallback nodes or a factory that creates them |
| `pluginFailurePlaceholder` | `(failure, ctx) => BaseRenderable \| BaseRenderable[] \| undefined` | no | Creates placeholder UI when a plugin throws |
| _…layout options_ | `RenderableOptions` | no | Standard layout props: `width`, `height`, `flexDirection`, `padding`, etc. |
Instance API
------------
| Member | Description |
| --- | --- |
| `mode` | Getter/setter. Changing the mode automatically refreshes the slot. |
| `refresh()` | Re-resolve plugins and reconcile mounted children. |
| `destroy()` | Inherits from `Renderable`. In addition to the standard cleanup, `SlotRenderable` unsubscribes from the registry, calls `onDeactivate` on active managed slots, then calls `onDispose` on all managed slots. Host-owned nodes (plain function contributions) are destroyed; plugin-owned nodes (managed slot contributions) are detached but left for the plugin to clean up in `onDispose`. |
`resolveCoreSlot`
-----------------
Resolve slot entries without mounting them. Useful when you need to inspect or render plugin output manually.
import { resolveCoreSlot } from "@opentui/core"
const entries = resolveCoreSlot(registry, "statusbar")
// Array<{ id: string, renderer: (ctx, data) => BaseRenderable }>
Plugin failure placeholders
---------------------------
If a plugin throws during slot render, you can provide host-controlled fallback UI for that plugin failure:
const slot = new SlotRenderable(renderer, {
registry,
name: "statusbar",
fallback: () => new TextRenderable(renderer, { id: "fallback", content: "fallback" }),
pluginFailurePlaceholder(failure, ctx) {
return new TextRenderable(renderer, {
id: `error-${failure.pluginId}`,
content: `plugin error: ${failure.pluginId}`,
})
},
})
Managed slot contributions
--------------------------
A core slot contribution can be a plain function or a managed slot object with lifecycle hooks:
registerCorePlugin(registry, {
id: "managed-plugin",
slots: {
statusbar: {
render(_ctx, data) {
return new TextRenderable(renderer, { id: "managed", content: "managed" })
},
onActivate(ctx) {
// plugin became active in this slot (visible)
},
onDeactivate(ctx) {
// plugin is no longer active (e.g. mode changed to single_winner and this plugin lost)
},
onDispose(ctx) {
// plugin is removed/disposed for this slot
},
},
},
})
### `CoreManagedSlot` interface
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `render` | `(ctx, data) => BaseRenderable` | yes | Creates the renderable node for this slot |
| `onActivate` | `(ctx) => void` | no | Called when the plugin becomes active (visible) in the slot |
| `onDeactivate` | `(ctx) => void` | no | Called when the plugin is no longer active (e.g. lost single\_winner) |
| `onDispose` | `(ctx) => void` | no | Called when the plugin is removed or the slot is destroyed |
### Node ownership
When a slot contribution is a **plain function**, the host owns the returned nodes. On deactivation or disposal, the host detaches and destroys them.
When a slot contribution is a **managed slot object**, the plugin owns the nodes. On deactivation the host detaches them but does _not_ destroy them — the plugin can reuse them if it becomes active again. On disposal, `onDispose` is called so the plugin can clean up.
Observability
-------------
registry.onPluginError((event) => {
console.error(event.pluginId, event.phase, event.source, event.error.message)
})
Example: `packages/core/src/examples/core-plugin-slots-demo.ts`
---
# Solid
Solid slots
===========
This page shows Solid integration for plugin slots.
Use slots when you want external modules to contribute `JSX.Element` UI in host-defined regions without forking your app. The host keeps ownership of layout and slot typing; plugins only see the context and props you expose.
If you have not read the shared model yet, start with [Plugin Slots](https://opentui.com/docs/plugins/slots)
.
Runtime-loaded external plugins
-------------------------------
If your app loads plugins from disk at runtime (for example `await import(fileUrl)`), add this import once in your app entry:
import "@opentui/solid/runtime-plugin-support"
This is a Bun drop-in that installs runtime transform support so external TS/TSX plugin modules use the same runtime instances as the host app (`@opentui/solid`, `@opentui/core`, `@opentui/core/3d`, `@opentui/core/testing`, `solid-js`, and `solid-js/store`).
Use this for plugin systems in both normal Bun runs and standalone compiled executables.
import "@opentui/solid/runtime-plugin-support"
const mod = await import(pathToFileURL(pluginPath).href)
registry.register(mod.loadExternalPlugin())
What Solid adds
---------------
* `createSolidSlotRegistry(renderer, context, options?)` — create a registry typed for `JSX.Element`. Accepts the same [`SlotRegistryOptions`](https://opentui.com/docs/plugins/slots#registry-options)
as `createSlotRegistry`.
* `Slot` — generic slot component that takes `registry` as a required prop
* `createSlot(registry, options?)` — optional convenience helper that returns a registry-bound `` component
* `SolidPlugin` — convenience type alias for a plugin that returns `JSX.Element`
* `@opentui/solid/runtime-plugin-support` — one-line runtime support for external plugin/module loading
Register plugins directly with `registry.register()` — no wrapper function is needed (unlike core’s `registerCorePlugin`).
Basic usage
-----------
import { createCliRenderer } from "@opentui/core"
import { createSolidSlotRegistry, Slot, render } from "@opentui/solid"
type Slots = {
statusbar: { user: string }
}
const context = { appName: "solid-app", version: "1.0.0" }
const renderer = await createCliRenderer()
const registry = createSolidSlotRegistry(renderer, context)
const unregister = registry.register({
id: "clock-plugin",
slots: {
statusbar(ctx, props) {
return {`${ctx.appName}:${props.user}`}
},
},
})
const AppSlot = Slot
const App = () => (
fallback-statusbar
)
render(() => , renderer)
Optional convenience helper
---------------------------
If you prefer not to pass `registry` each time, you can still bind one once:
const AppSlot = createSlot(registry)
`` props
--------------
| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `registry` | `SlotRegistry` | yes | Registry to resolve plugins from |
| `name` | `keyof Slots` | yes | Which slot to render |
| `mode` | `SlotMode` | no | `"append"` (default), `"replace"`, or `"single_winner"`. See [slot modes](https://opentui.com/docs/plugins/slots#slot-modes)
. |
| `pluginFailurePlaceholder` | `(failure: PluginErrorEvent) => JSX.Element` | no | Per-slot placeholder UI when a plugin throws |
| `children` | `JSX.Element` | no | Fallback UI |
| _remaining_ | `Slots[name]` | — | Slot-specific props forwarded to plugin renderers |
`SolidSlotOptions` (for `createSlot`)
-------------------------------------
| Option | Type | Required | Description |
| --- | --- | --- | --- |
| `pluginFailurePlaceholder` | `(failure: PluginErrorEvent) => JSX.Element` | no | Creates placeholder UI when a plugin throws |
Plugin failure placeholders
---------------------------
const Slot = createSlot(registry, {
pluginFailurePlaceholder(failure) {
return {`plugin-error:${failure.pluginId}:${failure.phase}`}
},
})
If a plugin throws, the slot renders the placeholder. If no placeholder is provided (or it returns `null`), the slot falls back to `children`.
Plugins that throw during the initial render call are caught inline. Plugins that throw during a Solid re-render are caught by an internal `` that reports the error to the registry.
Observability
-------------
registry.onPluginError((event) => {
console.error(event.pluginId, event.phase, event.source, event.error.message)
})
Example: `packages/solid/examples/components/plugin-slots-demo.tsx`
---
# Textarea
Textarea
========
Multi-line text input with cursor, selection, and rich keybindings.
Basic usage
-----------
### Renderable API
import { TextareaRenderable, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const textarea = new TextareaRenderable(renderer, {
id: "notes",
width: 50,
height: 6,
placeholder: "Type notes here...",
backgroundColor: "#1a1a1a",
focusedBackgroundColor: "#222222",
textColor: "#FFFFFF",
cursorColor: "#00FF88",
})
renderer.root.add(textarea)
textarea.focus()
Submit handling
---------------
Bind a submit action and listen for `onSubmit`:
import { TextareaRenderable } from "@opentui/core"
const textarea = new TextareaRenderable(renderer, {
width: 50,
height: 6,
onSubmit: () => {
console.log("Submitted:", textarea.plainText)
},
keyBindings: [{ name: "return", ctrl: true, action: "submit" }],
})
Placeholder styling
-------------------
const textarea = new TextareaRenderable(renderer, {
width: 40,
height: 4,
placeholder: "Type here",
placeholderColor: "#666666",
})
Construct API
-------------
> Not available yet. Use `TextareaRenderable` for now.
Properties
----------
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `width` | `number` or `string` | \- | Width in characters or percentage |
| `height` | `number` or `string` | \- | Height in rows or percentage |
| `initialValue` | `string` | `""` | Initial text content |
| `placeholder` | `string`, `StyledText`, or `null` | `null` | Placeholder content |
| `placeholderColor` | `string` or `RGBA` | `#666666` | Placeholder color |
| `backgroundColor` | `string` or `RGBA` | transparent | Background when unfocused |
| `textColor` | `string` or `RGBA` | `#FFFFFF` | Text color when unfocused |
| `focusedBackgroundColor` | `string` or `RGBA` | transparent | Background when focused |
| `focusedTextColor` | `string` or `RGBA` | `#FFFFFF` | Text color when focused |
| `wrapMode` | `"none"`, `"char"`, or `"word"` | `"word"` | Line wrapping mode |
| `selectionBg` | `string` or `RGBA` | \- | Selection background |
| `selectionFg` | `string` or `RGBA` | \- | Selection foreground |
| `cursorColor` | `string` or `RGBA` | `#FFFFFF` | Cursor color |
| `cursorStyle` | `CursorStyleOptions` | \- | Cursor style and blinking |
| `keyBindings` | `KeyBinding[]` | \- | Custom keybindings |
| `keyAliasMap` | `KeyAliasMap` | \- | Key alias mapping |
| `onSubmit` | `(event: SubmitEvent) => void` | \- | Submit handler |
| `onContentChange` | `(event: ContentChangeEvent) => void` | \- | Fired on content changes |
| `onCursorChange` | `(event: CursorChangeEvent) => void` | \- | Fired on cursor movement |
Useful properties
-----------------
| Property | Type | Description |
| --- | --- | --- |
| `plainText` | `string` | Current text content |
| `cursorOffset` | `number` | Cursor offset in the buffer |
---
# Input
Input
=====
Text input field with cursor, placeholder text, and focus states. Focus the input to receive keyboard input.
Basic usage
-----------
### Renderable api
import { InputRenderable, InputRenderableEvents, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const input = new InputRenderable(renderer, {
id: "name-input",
width: 25,
placeholder: "Enter your name...",
})
input.on(InputRenderableEvents.CHANGE, (value) => {
console.log("Input value:", value)
})
input.focus()
renderer.root.add(input)
### Construct api
import { Input, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const input = Input({
placeholder: "Enter your name...",
width: 25,
})
input.focus()
renderer.root.add(input)
Focus states
------------
The input changes appearance when focused:
const input = new InputRenderable(renderer, {
id: "styled-input",
width: 30,
placeholder: "Type here...",
backgroundColor: "#1a1a1a",
focusedBackgroundColor: "#2a2a2a",
textColor: "#FFFFFF",
cursorColor: "#00FF00",
})
Events
------
### Input event
Fires on every keystroke as the value changes:
import { InputRenderableEvents } from "@opentui/core"
input.on(InputRenderableEvents.INPUT, (value: string) => {
console.log("Current value:", value)
})
### Change event
Fires when the input loses focus (blur) or when you press Enter, but only if the value changed since focus:
input.on(InputRenderableEvents.CHANGE, (value: string) => {
console.log("Value committed:", value)
})
### Enter event
Fires when the user presses Enter/Return:
input.on(InputRenderableEvents.ENTER, (value: string) => {
console.log("Submitted value:", value)
})
### Getting the current value
const currentValue = input.value
### Setting the value
input.value = "New value"
Properties
----------
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `width` | `number` | \- | Input field width |
| `value` | `string` | `""` | Initial text value |
| `placeholder` | `string` | `""` | Placeholder text when empty |
| `maxLength` | `number` | `1000` | Maximum number of characters |
| `backgroundColor` | `string \| RGBA` | \- | Background when unfocused |
| `focusedBackgroundColor` | `string \| RGBA` | \- | Background when focused |
| `textColor` | `string \| RGBA` | \- | Text color |
| `cursorColor` | `string \| RGBA` | \- | Cursor color |
| `position` | `"static" \| "relative" \| "absolute"` | `"relative"` | Positioning mode |
Example: Login form
-------------------
import { Box, Text, Input, delegate, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
function LabeledInput(props: { id: string; label: string; placeholder: string }) {
return delegate(
{ focus: `${props.id}-input` },
Box(
{ flexDirection: "row", marginBottom: 1 },
Text({ content: props.label.padEnd(12), fg: "#888888" }),
Input({
id: `${props.id}-input`,
placeholder: props.placeholder,
width: 20,
backgroundColor: "#222",
focusedBackgroundColor: "#333",
textColor: "#FFF",
cursorColor: "#0F0",
}),
),
)
}
const usernameInput = LabeledInput({
id: "username",
label: "Username:",
placeholder: "Enter username",
})
const passwordInput = LabeledInput({
id: "password",
label: "Password:",
placeholder: "Enter password",
})
const form = Box(
{
width: 40,
borderStyle: "rounded",
title: "Login",
padding: 1,
},
usernameInput,
passwordInput,
)
// Focus the username input
usernameInput.focus()
renderer.root.add(form)
Tab navigation
--------------
Add tab navigation between inputs:
const inputs = [usernameInput, passwordInput]
let focusIndex = 0
renderer.keyInput.on("keypress", (key) => {
if (key.name === "tab") {
focusIndex = (focusIndex + 1) % inputs.length
inputs[focusIndex].focus()
}
})
---
# Select
Select
======
A vertical list for choosing from multiple options. Focus the select to enable keyboard input.
Basic usage
-----------
### Renderable API
import { SelectRenderable, SelectRenderableEvents, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const menu = new SelectRenderable(renderer, {
id: "menu",
width: 30,
height: 8,
options: [\
{ name: "New File", description: "Create a new file" },\
{ name: "Open File", description: "Open an existing file" },\
{ name: "Save", description: "Save current file" },\
{ name: "Exit", description: "Exit the application" },\
],
})
menu.on(SelectRenderableEvents.ITEM_SELECTED, (index, option) => {
console.log("Selected:", option.name)
})
menu.focus()
renderer.root.add(menu)
### Construct API
import { Select, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const menu = Select({
width: 30,
height: 8,
options: [\
{ name: "Option 1", description: "First option" },\
{ name: "Option 2", description: "Second option" },\
{ name: "Option 3", description: "Third option" },\
],
})
menu.focus()
renderer.root.add(menu)
Keyboard navigation
-------------------
When focused, the select responds to these keys:
| Key | Action |
| --- | --- |
| `Up` / `k` | Move selection up |
| `Down` / `j` | Move selection down |
| `Shift+Up` / `Shift+Down` | Fast scroll (5 items) |
| `Enter` | Select current item |
Events
------
### Item selected
Fires when the user presses Enter on an option:
import { SelectRenderableEvents } from "@opentui/core"
menu.on(SelectRenderableEvents.ITEM_SELECTED, (index: number, option: SelectOption) => {
console.log(`Selected index ${index}: ${option.name}`)
})
### Selection changed
Fires when the highlighted item changes:
menu.on(SelectRenderableEvents.SELECTION_CHANGED, (index: number, option: SelectOption) => {
console.log(`Highlighted: ${option.name}`)
// Update a preview pane, for example
})
Option structure
----------------
interface SelectOption {
name: string // Display text
description: string // Displays below the name when showDescription is true
value?: any // Optional value
}
Styling
-------
const styledMenu = new SelectRenderable(renderer, {
id: "styled-menu",
width: 40,
height: 10,
options: [...],
backgroundColor: "#1a1a1a",
selectedBackgroundColor: "#333366",
selectedTextColor: "#FFFFFF",
textColor: "#AAAAAA",
descriptionColor: "#666666",
})
Properties
----------
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `width` | `number` | \- | Component width |
| `height` | `number` | \- | Component height |
| `options` | `SelectOption[]` | `[]` | Available options |
| `selectedIndex` | `number` | `0` | Initially selected index |
| `backgroundColor` | `string \| RGBA` | `transparent` | Background color |
| `textColor` | `string \| RGBA` | `#FFFFFF` | Normal text color |
| `focusedBackgroundColor` | `string \| RGBA` | `#1a1a1a` | Background when focused |
| `focusedTextColor` | `string \| RGBA` | `#FFFFFF` | Text color when focused |
| `selectedBackgroundColor` | `string \| RGBA` | `#334455` | Selected item background |
| `selectedTextColor` | `string \| RGBA` | `#FFFF00` | Selected item text color |
| `descriptionColor` | `string \| RGBA` | `#888888` | Description text color |
| `selectedDescriptionColor` | `string \| RGBA` | `#CCCCCC` | Selected item description color |
| `showDescription` | `boolean` | `true` | Show option descriptions |
| `showScrollIndicator` | `boolean` | `false` | Show scroll position indicator |
| `wrapSelection` | `boolean` | `false` | Wrap selection at list boundaries |
| `itemSpacing` | `number` | `0` | Spacing between items |
| `fastScrollStep` | `number` | `5` | Items to skip with Shift+Up/Down |
Example: file menu
------------------
import { Box, Select, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const fileMenu = Select({
width: 25,
height: 12,
options: [\
{ name: "New", description: "Create new file (Ctrl+N)" },\
{ name: "Open...", description: "Open file (Ctrl+O)" },\
{ name: "Save", description: "Save file (Ctrl+S)" },\
{ name: "Save As...", description: "Save with new name" },\
{ name: "---", description: "" }, // Separator (visual only)\
{ name: "Exit", description: "Quit application (Ctrl+Q)" },\
],
})
const menuPanel = Box(
{
borderStyle: "single",
borderColor: "#666",
},
fileMenu,
)
fileMenu.focus()
renderer.root.add(menuPanel)
Programmatic control
--------------------
// Get current selection index
const currentIndex = menu.getSelectedIndex()
// Get currently selected option
const option = menu.getSelectedOption()
// Set selection programmatically
menu.setSelectedIndex(2)
// Navigate programmatically
menu.moveUp() // Move up one item
menu.moveDown() // Move down one item
menu.moveUp(3) // Move up multiple items
menu.selectCurrent() // Trigger selection of current item
// Update options dynamically
menu.options = [\
{ name: "New Option 1", description: "First" },\
{ name: "New Option 2", description: "Second" },\
]
// Toggle display options
menu.showDescription = false
menu.showScrollIndicator = true
menu.wrapSelection = true
---
# Box
Box
===
A container component with borders, background colors, and layout capabilities. Use it to create panels, frames, and organized sections.
Basic usage
-----------
### Renderable API
import { BoxRenderable, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const panel = new BoxRenderable(renderer, {
id: "panel",
width: 30,
height: 10,
backgroundColor: "#333366",
borderStyle: "double",
borderColor: "#FFFFFF",
})
renderer.root.add(panel)
### Construct API
import { Box, Text, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
renderer.root.add(
Box(
{
width: 30,
height: 10,
backgroundColor: "#333366",
borderStyle: "rounded",
},
Text({ content: "Inside the box!" }),
),
)
Border styles
-------------
// No border
{
border: false
}
// Simple border (default style)
{
border: true
}
// Specific border styles
{
borderStyle: "single"
} // Single line: ┌─┐│└─┘
{
borderStyle: "double"
} // Double line: ╔═╗║╚═╝
{
borderStyle: "rounded"
} // Rounded corners: ╭─╮│╰─╯
{
borderStyle: "heavy"
} // Heavy lines: ┏━┓┃┗━┛
Titles
------
Add a title and bottom title to the box border:
const panel = new BoxRenderable(renderer, {
id: "settings",
width: 40,
height: 15,
borderStyle: "rounded",
title: "Settings",
titleAlignment: "center",
bottomTitle: "Footer",
bottomTitleAlignment: "center",
})
### Title alignment (both top and bottom titles)
{
titleAlignment: "left"
} // ┌─ Title ────────┐
{
titleAlignment: "center"
} // ┌──── Title ─────┐
{
titleAlignment: "right"
} // ┌────────── Title ┐
{
bottomTitleAlignment: "left"
} // └─ Title ────────┘
{
bottomTitleAlignment: "center"
} // └──── Title ─────┘
{
bottomTitleAlignment: "right"
} // └────────── Title ┘
Layout container
----------------
Box works as a flex container for child elements:
const container = Box(
{
flexDirection: "column",
justifyContent: "space-between",
alignItems: "stretch",
width: 50,
height: 20,
padding: 1,
gap: 1,
},
Text({ content: "Header" }),
Box({ flexGrow: 1, backgroundColor: "#222" }, Text({ content: "Content area" })),
Text({ content: "Footer" }),
)
Mouse events
------------
Handle mouse interactions on the box:
const button = new BoxRenderable(renderer, {
id: "button",
width: 12,
height: 3,
border: true,
backgroundColor: "#444",
onMouseDown: () => {
console.log("Button clicked!")
},
onMouseOver: () => {
button.backgroundColor = "#666"
},
onMouseOut: () => {
button.backgroundColor = "#444"
},
})
Properties
----------
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `width` | `number \| string` | \- | Width in characters or percentage |
| `height` | `number \| string` | \- | Height in rows or percentage |
| `backgroundColor` | `string \| RGBA` | \- | Background fill color |
| `border` | `boolean` | `false` | Show border |
| `borderStyle` | `string` | `"single"` | Border style |
| `borderColor` | `string \| RGBA` | \- | Border color |
| `title` | `string` | \- | Title text in border |
| `titleAlignment` | `string` | `"left"` | Title position |
| `bottomTitle` | `string` | \- | Bottom title text in border |
| `bottomTitleAlignment` | `string` | `"left"` | Bottom title position |
| `padding` | `number` | `0` | Internal padding |
| `gap` | `number \| string` | \- | Gap between children |
| `flexDirection` | `string` | `"column"` | Child layout direction |
| `justifyContent` | `string` | `"flex-start"` | Main axis alignment |
| `alignItems` | `string` | `"stretch"` | Cross axis alignment |
Example: Card component
-----------------------
import { Box, Text, t, bold, fg } from "@opentui/core"
function Card(props: { title: string; description: string }) {
return Box(
{
width: 40,
borderStyle: "rounded",
borderColor: "#666",
padding: 1,
margin: 1,
},
Text({
content: t`${bold(fg("#00FFFF")(props.title))}`,
}),
Text({
content: props.description,
fg: "#AAAAAA",
}),
)
}
renderer.root.add(
Box(
{ flexDirection: "row", flexWrap: "wrap" },
Card({ title: "Feature 1", description: "Description of feature 1" }),
Card({ title: "Feature 2", description: "Description of feature 2" }),
Card({ title: "Feature 3", description: "Description of feature 3" }),
),
)
---
# TabSelect
TabSelect
=========
Horizontal tab-based selection component with descriptions and scroll support. The component must be focused to receive keyboard input.
Basic Usage
-----------
### Renderable API
import { TabSelectRenderable, TabSelectRenderableEvents, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const tabs = new TabSelectRenderable(renderer, {
id: "tabs",
width: 60,
options: [\
{ name: "Home", description: "Dashboard and overview" },\
{ name: "Files", description: "File management" },\
{ name: "Settings", description: "Application settings" },\
],
tabWidth: 20,
})
tabs.on(TabSelectRenderableEvents.ITEM_SELECTED, (index, option) => {
console.log("Tab selected:", option.name)
})
tabs.focus()
renderer.root.add(tabs)
### Construct API
import { TabSelect, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const tabs = TabSelect({
width: 60,
tabWidth: 15,
options: [\
{ name: "Tab 1", description: "First tab" },\
{ name: "Tab 2", description: "Second tab" },\
{ name: "Tab 3", description: "Third tab" },\
],
})
tabs.focus()
renderer.root.add(tabs)
Keyboard Navigation
-------------------
When focused, the tab select responds to these keys:
| Key | Action |
| --- | --- |
| `Left` / `[` | Move to previous tab |\
| `Right` / `]` | Move to next tab |
| `Enter` | Select current tab |
Events
------
### Item Selected
Emitted when the user presses Enter on a tab:
import { TabSelectRenderableEvents, type TabSelectOption } from "@opentui/core"
tabs.on(TabSelectRenderableEvents.ITEM_SELECTED, (index: number, option: TabSelectOption) => {
console.log(`Selected tab ${index}: ${option.name}`)
// Switch to the corresponding panel
})
### Selection Changed
Emitted when the highlighted tab changes:
import { TabSelectRenderableEvents, type TabSelectOption } from "@opentui/core"
tabs.on(TabSelectRenderableEvents.SELECTION_CHANGED, (index: number, option: TabSelectOption) => {
console.log(`Hovering: ${option.name}`)
})
Properties
----------
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `width` | `number` | \- | Total component width |
| `options` | `TabSelectOption[]` | `[]` | Available tabs |
| `tabWidth` | `number` | `20` | Width of each tab |
| `backgroundColor` | `string \| RGBA` | `transparent` | Background color |
| `textColor` | `string \| RGBA` | `#FFFFFF` | Normal tab text |
| `focusedBackgroundColor` | `string \| RGBA` | `#1a1a1a` | Background color when focused |
| `focusedTextColor` | `string \| RGBA` | `#FFFFFF` | Text color when focused |
| `selectedBackgroundColor` | `string \| RGBA` | `#334455` | Selected tab background |
| `selectedTextColor` | `string \| RGBA` | `#FFFF00` | Selected tab text |
| `selectedDescriptionColor` | `string \| RGBA` | `#CCCCCC` | Description text color |
| `showScrollArrows` | `boolean` | `true` | Show scroll indicators |
| `showDescription` | `boolean` | `true` | Show tab descriptions |
| `showUnderline` | `boolean` | `true` | Show underline on selected tab |
| `wrapSelection` | `boolean` | `false` | Wrap around when navigating |
| `keyBindings` | `TabSelectKeyBinding[]` | \- | Custom key bindings |
| `keyAliasMap` | `KeyAliasMap` | \- | Key alias mappings |
Example: Tabbed Interface
-------------------------
import { Box, Text, TabSelect, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
// Create content panels
const panels = {
home: Box({ padding: 1 }, Text({ content: "Home content here" })),
files: Box({ padding: 1 }, Text({ content: "File browser here" })),
settings: Box({ padding: 1 }, Text({ content: "Settings form here" })),
}
// Create the tabbed container
const container = Box({
width: 60,
height: 20,
borderStyle: "rounded",
})
const tabs = TabSelect({
width: 60,
tabWidth: 20,
options: [\
{ name: "Home", description: "Dashboard" },\
{ name: "Files", description: "Browse files" },\
{ name: "Settings", description: "Preferences" },\
],
})
// Content area
let currentPanel = panels.home
const contentArea = Box({
flexGrow: 1,
padding: 1,
})
contentArea.add(currentPanel)
// Handle tab changes
tabs.on("itemSelected", (index, option) => {
// Remove current panel
if (currentPanel) {
contentArea.remove(currentPanel.id)
}
// Add new panel based on selection
switch (option.name) {
case "Home":
currentPanel = panels.home
break
case "Files":
currentPanel = panels.files
break
case "Settings":
currentPanel = panels.settings
break
}
contentArea.add(currentPanel)
})
container.add(tabs)
container.add(contentArea)
tabs.focus()
renderer.root.add(container)
Programmatic Control
--------------------
// Get current tab index
const currentIndex = tabs.getSelectedIndex()
// Set tab programmatically
tabs.setSelectedIndex(1)
// Update tabs dynamically
tabs.setOptions([\
{ name: "New Tab 1", description: "Updated" },\
{ name: "New Tab 2", description: "Also updated" },\
])
Scroll Behavior
---------------
When there are more tabs than fit in the width, the component automatically handles horizontal scrolling as you navigate with the keyboard.
---
# ScrollBar
ScrollBar
=========
A standalone scrollbar component with optional arrows, keyboard navigation, and a draggable thumb.
Basic usage
-----------
### Renderable API
import { ScrollBarRenderable, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const scrollbar = new ScrollBarRenderable(renderer, {
id: "scrollbar",
orientation: "vertical",
height: 10,
showArrows: true,
trackOptions: {
backgroundColor: "#222222",
foregroundColor: "#888888",
},
onChange: (position) => {
console.log("Scroll position:", position)
},
})
// Connect the scrollbar to your content
scrollbar.scrollSize = 200
scrollbar.viewportSize = 20
scrollbar.scrollPosition = 0
renderer.root.add(scrollbar)
scrollbar.focus()
Keyboard controls
-----------------
When focused, the scrollbar responds to:
* `Up`/`Down` or `k`/`j` for vertical bars
* `Left`/`Right` or `h`/`l` for horizontal bars
* `PageUp`/`PageDown` for larger jumps
* `Home`/`End` to jump to start/end
Construct API
-------------
> Not available yet. Use `ScrollBarRenderable` for now.
Properties
----------
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `orientation` | `"vertical"` or `"horizontal"` | \- | Scrollbar direction |
| `showArrows` | `boolean` | `false` | Show arrow buttons |
| `arrowOptions` | `ArrowOptions` | \- | Styling for arrow buttons |
| `trackOptions` | `Partial` | \- | Styling for the track and thumb |
| `scrollSize` | `number` | `0` | Total scrollable size |
| `viewportSize` | `number` | `0` | Visible size of the viewport |
| `scrollPosition` | `number` | `0` | Current scroll position |
| `scrollStep` | `number` | \- | Step size when `scrollBy(..., "step")` |
| `onChange` | `(position: number) => void` | \- | Fired when scroll position changes |
---
# Slider
Slider
======
A draggable slider for continuous values. Supports vertical and horizontal orientations.
Basic usage
-----------
### Renderable API
import { SliderRenderable, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const slider = new SliderRenderable(renderer, {
id: "volume",
orientation: "horizontal",
width: 30,
height: 1,
min: 0,
max: 100,
value: 25,
onChange: (value) => {
console.log("Value:", value)
},
})
renderer.root.add(slider)
Vertical slider
---------------
const slider = new SliderRenderable(renderer, {
orientation: "vertical",
width: 2,
height: 10,
min: 0,
max: 1,
value: 0.5,
})
Construct API
-------------
> Not available yet. Use `SliderRenderable` for now.
Properties
----------
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `orientation` | `"vertical"` or `"horizontal"` | \- | Slider direction |
| `value` | `number` | `min` | Current value |
| `min` | `number` | `0` | Minimum value |
| `max` | `number` | `100` | Maximum value |
| `viewPortSize` | `number` | range \* 0.1 | Thumb size relative to content |
| `backgroundColor` | `string` or `RGBA` | \- | Track color |
| `foregroundColor` | `string` or `RGBA` | \- | Thumb color |
| `onChange` | `(value: number) => void` | \- | Fired when value changes |
---
# Markdown
Markdown
========
Render markdown content with syntax-aware styling and optional Tree-sitter highlighting for code blocks.
Basic usage
-----------
### Renderable API
import { MarkdownRenderable, SyntaxStyle, RGBA, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const syntaxStyle = SyntaxStyle.fromStyles({
"markup.heading.1": { fg: RGBA.fromHex("#58A6FF"), bold: true },
"markup.list": { fg: RGBA.fromHex("#FF7B72") },
"markup.raw": { fg: RGBA.fromHex("#A5D6FF") },
default: { fg: RGBA.fromHex("#E6EDF3") },
})
const markdown = new MarkdownRenderable(renderer, {
id: "readme",
width: 60,
content: "# Hello\n\n- One\n- Two\n\n```ts\nconst x = 1\n```",
syntaxStyle,
})
renderer.root.add(markdown)
Fenced language normalization
-----------------------------
Code fence info strings are normalized before Tree-sitter highlighting.
* `tsx` -> `typescriptreact`
* `.jsx` -> `javascriptreact`
* `TSX title=Button.tsx` -> `typescriptreact`
* `Dockerfile` -> `dockerfile`
Normalization uses `infoStringToFiletype()`. You can extend or override mappings at runtime:
import { extensionToFiletype, basenameToFiletype } from "@opentui/core"
extensionToFiletype.set("templ", "html")
basenameToFiletype.set("mytoolrc", "yaml")
Concealment
-----------
Hide markdown markers (backticks, emphasis markers, etc.) when `conceal` is true:
const markdown = new MarkdownRenderable(renderer, {
content: "**bold** and `code`",
syntaxStyle,
conceal: true,
})
Use `concealCode` to control concealment inside fenced code blocks independently (`false` by default).
Streaming updates
-----------------
Enable streaming mode for incremental updates. Keep it `true` while appending chunks, then set `markdown.streaming = false` when complete to finalize trailing block parsing. Tables include trailing partial rows, with missing cells rendered empty.
const markdown = new MarkdownRenderable(renderer, {
content: "",
syntaxStyle,
streaming: true,
})
markdown.content += "# Live log\n"
markdown.content += "- line 1\n"
markdown.streaming = false
Markdown tables
---------------
Markdown tables support `tableOptions` for sizing, wrapping, borders, and selection.
const markdown = new MarkdownRenderable(renderer, {
content: "| Service | Status |\n| --- | --- |\n| api | ok |",
syntaxStyle,
tableOptions: {
widthMode: "full",
columnFitter: "balanced",
wrapMode: "word",
cellPadding: 1,
borders: true,
outerBorder: true,
borderStyle: "rounded",
borderColor: "#6b7280",
selectable: true,
},
})
### `tableOptions`
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `widthMode` | `"content" \| "full"` | `"full"` | `"full"` expands columns to fill available width |
| `columnFitter` | `"proportional" \| "balanced"` | `"proportional"` | How columns shrink when space is constrained |
| `wrapMode` | `"none" \| "char" \| "word"` | `"word"` | Wrapping mode inside each table cell |
| `cellPadding` | `number` | `0` | Padding on all sides of each cell |
| `borders` | `boolean` | `true` | Enable inner and outer borders |
| `outerBorder` | `boolean` | `borders` | Override outer border visibility |
| `borderStyle` | `BorderStyle` | `"single"` | Table border character set |
| `borderColor` | `ColorInput` | conceal fg or `#888888` | Border color for markdown tables |
| `selectable` | `boolean` | `true` | Enable table cell text selection |
Custom node rendering
---------------------
Override rendering for a token and fall back to default rendering:
const markdown = new MarkdownRenderable(renderer, {
content: "# Title\n\nHello",
syntaxStyle,
renderNode: (token, context) => {
if (token.type === "heading") {
return context.defaultRender()
}
return undefined
},
})
Construct API
-------------
> Not available yet. Use `MarkdownRenderable` for now.
Properties
----------
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `content` | `string` | `""` | Markdown source |
| `syntaxStyle` | `SyntaxStyle` | required | Style definitions for tokens |
| `conceal` | `boolean` | `true` | Hide markdown markers in markdown text |
| `concealCode` | `boolean` | `false` | Hide markers inside fenced code blocks |
| `streaming` | `boolean` | `false` | Incremental mode; set `false` to finalize |
| `tableOptions` | `MarkdownTableOptions` | \- | Options for markdown table rendering |
| `treeSitterClient` | `TreeSitterClient` | \- | Custom Tree-sitter client for code blocks |
| `renderNode` | `(token: Token, context: RenderNodeContext) => Renderable \| null \| undefined` | \- | Custom render hook per markdown block |
---
# FrameBuffer
FrameBuffer
===========
A low-level rendering surface for custom graphics and complex visual effects. FrameBuffer provides a 2D array of cells with methods optimized for performance and memory.
Basic usage
-----------
### Renderable API
import { FrameBufferRenderable, RGBA, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const canvas = new FrameBufferRenderable(renderer, {
id: "canvas",
width: 50,
height: 20,
})
// Draw on the frame buffer
canvas.frameBuffer.fillRect(5, 2, 20, 10, RGBA.fromHex("#FF0000"))
canvas.frameBuffer.drawText("Hello!", 8, 6, RGBA.fromHex("#FFFFFF"))
renderer.root.add(canvas)
### Construct API
import { FrameBuffer, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
renderer.root.add(
FrameBuffer({
width: 50,
height: 20,
}),
)
Drawing methods
---------------
### setCell
Set a single cell’s content and colors:
canvas.frameBuffer.setCell(
x, // X position
y, // Y position
char, // Character to display
fg, // Foreground color (RGBA)
bg, // Background color (RGBA)
attributes, // Text attributes (optional, default: 0)
)
// Example
canvas.frameBuffer.setCell(10, 5, "@", RGBA.fromHex("#FFFF00"), RGBA.fromHex("#000000"))
### setCellWithAlphaBlending
Set a cell with alpha blending for transparency effects:
const semiTransparent = RGBA.fromValues(1.0, 0.0, 0.0, 0.5)
const transparent = RGBA.fromValues(0, 0, 0, 0)
canvas.frameBuffer.setCellWithAlphaBlending(10, 5, " ", transparent, semiTransparent)
### drawText
Draw a string of text at a position:
canvas.frameBuffer.drawText(
text, // String to draw
x, // Starting X position
y, // Y position
fg, // Text color (RGBA)
bg, // Background color (RGBA, optional)
attributes, // Text attributes (optional, default: 0)
)
// Example
canvas.frameBuffer.drawText("Score: 100", 2, 1, RGBA.fromHex("#00FF00"))
### fillRect
Fill a rectangular area with a color:
canvas.frameBuffer.fillRect(
x, // X position
y, // Y position
width, // Rectangle width
height, // Rectangle height
color, // Fill color (RGBA)
)
// Example: Draw a red rectangle
canvas.frameBuffer.fillRect(10, 5, 20, 8, RGBA.fromHex("#FF0000"))
### drawFrameBuffer
Copy another frame buffer onto this one:
canvas.frameBuffer.drawFrameBuffer(
destX, // Destination X
destY, // Destination Y
sourceBuffer, // Source FrameBuffer (OptimizedBuffer)
sourceX, // Source X offset (optional)
sourceY, // Source Y offset (optional)
sourceWidth, // Width to copy (optional)
sourceHeight, // Height to copy (optional)
)
### colorMatrix / colorMatrixUniform
Apply native 4x4 RGBA matrix transforms for post-processing effects. Use `colorMatrixUniform` for full-buffer transforms, and `colorMatrix` when you want to target specific cells.
import { INVERT_MATRIX, TargetChannel } from "@opentui/core"
// Full-buffer transform
canvas.frameBuffer.colorMatrixUniform(INVERT_MATRIX, 1.0, TargetChannel.Both)
// Per-cell transform with explicit mask
const cellMask = new Float32Array([10, 5, 1.0, 11, 5, 0.5])
canvas.frameBuffer.colorMatrix(INVERT_MATRIX, cellMask, 1.0, TargetChannel.FG)
See [Color matrix reference](https://opentui.com/docs/reference/color-matrix)
for matrix layout, mask format, and behavior details.
Properties
----------
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `width` | `number` | \- | Buffer width in characters (required) |
| `height` | `number` | \- | Buffer height in rows (required) |
| `respectAlpha` | `boolean` | `false` | Enable alpha blending when drawing |
| `position` | `string` | `"relative"` | Positioning mode |
| `left`, `top`, `right`, `bottom` | `number` | \- | Position offsets |
Example: Simple game canvas
---------------------------
import { FrameBufferRenderable, RGBA, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const gameCanvas = new FrameBufferRenderable(renderer, {
id: "game",
width: 40,
height: 20,
position: "absolute",
left: 5,
top: 2,
})
// Game state
let playerX = 20
let playerY = 10
function render() {
const fb = gameCanvas.frameBuffer
const BG = RGBA.fromHex("#111111")
// Clear the canvas
fb.fillRect(0, 0, 40, 20, BG)
// Draw border
for (let x = 0; x < 40; x++) {
fb.setCell(x, 0, "-", RGBA.fromHex("#444444"), BG)
fb.setCell(x, 19, "-", RGBA.fromHex("#444444"), BG)
}
for (let y = 0; y < 20; y++) {
fb.setCell(0, y, "|", RGBA.fromHex("#444444"), BG)
fb.setCell(39, y, "|", RGBA.fromHex("#444444"), BG)
}
// Draw player
fb.setCell(playerX, playerY, "@", RGBA.fromHex("#00FF00"), BG)
// Draw score
fb.drawText("Score: 0", 2, 0, RGBA.fromHex("#FFFF00"))
}
// Handle input
renderer.keyInput.on("keypress", (key) => {
switch (key.name) {
case "up":
playerY = Math.max(1, playerY - 1)
break
case "down":
playerY = Math.min(18, playerY + 1)
break
case "left":
playerX = Math.max(1, playerX - 1)
break
case "right":
playerX = Math.min(38, playerX + 1)
break
}
render()
})
render()
renderer.root.add(gameCanvas)
Example: Progress bar
---------------------
const EMPTY_BG = RGBA.fromHex("#222222")
function drawProgressBar(fb, x, y, width, progress, color) {
const filled = Math.floor(width * progress)
// Draw filled portion
for (let i = 0; i < filled; i++) {
fb.setCell(x + i, y, "█", color, EMPTY_BG)
}
// Draw empty portion
for (let i = filled; i < width; i++) {
fb.setCell(x + i, y, "░", RGBA.fromHex("#333333"), EMPTY_BG)
}
}
// Usage
drawProgressBar(canvas.frameBuffer, 5, 10, 30, 0.75, RGBA.fromHex("#00FF00"))
Performance tips
----------------
1. **Batch updates**: Make multiple changes to the frame buffer before the next render cycle
2. **Minimize fillRect calls**: Use individual setCell calls for complex shapes
3. **Reuse RGBA objects**: Create color constants instead of calling `fromHex` repeatedly
// Good: Create once, reuse
const RED = RGBA.fromHex("#FF0000")
const GREEN = RGBA.fromHex("#00FF00")
const BG = RGBA.fromHex("#000000")
for (let i = 0; i < 100; i++) {
fb.setCell(i, 5, "*", RED, BG)
}
// Avoid: Creating new RGBA objects in loops
for (let i = 0; i < 100; i++) {
fb.setCell(i, 5, "*", RGBA.fromHex("#FF0000"), RGBA.fromHex("#000000")) // Creates 200 objects
}
---
# Code
Code
====
Displays syntax-highlighted code using Tree-sitter. Supports many languages with accurate, fast highlighting.
Basic usage
-----------
### Renderable API
import { CodeRenderable, createCliRenderer, SyntaxStyle, RGBA } from "@opentui/core"
const renderer = await createCliRenderer()
const syntaxStyle = SyntaxStyle.fromStyles({
keyword: { fg: RGBA.fromHex("#FF7B72"), bold: true },
string: { fg: RGBA.fromHex("#A5D6FF") },
comment: { fg: RGBA.fromHex("#8B949E"), italic: true },
number: { fg: RGBA.fromHex("#79C0FF") },
function: { fg: RGBA.fromHex("#D2A8FF") },
default: { fg: RGBA.fromHex("#E6EDF3") },
})
const code = new CodeRenderable(renderer, {
id: "code",
content: `function hello() {
// This is a comment
const message = "Hello, world!"
return message
}`,
filetype: "javascript",
syntaxStyle,
width: 50,
height: 10,
})
renderer.root.add(code)
### Construct API
import { Code, Box, createCliRenderer, SyntaxStyle, RGBA } from "@opentui/core"
const renderer = await createCliRenderer()
const syntaxStyle = SyntaxStyle.fromStyles({
keyword: { fg: RGBA.fromHex("#FF7B72"), bold: true },
string: { fg: RGBA.fromHex("#A5D6FF") },
default: { fg: RGBA.fromHex("#E6EDF3") },
})
renderer.root.add(
Box(
{ border: true, width: 50, height: 10 },
Code({
content: 'const x = "hello"',
filetype: "javascript",
syntaxStyle,
}),
),
)
Creating syntax styles
----------------------
Use `SyntaxStyle.fromStyles()` to define colors and attributes for syntax tokens:
import { SyntaxStyle, RGBA, parseColor } from "@opentui/core"
const syntaxStyle = SyntaxStyle.fromStyles({
// Basic tokens
keyword: { fg: RGBA.fromHex("#FF7B72"), bold: true },
"keyword.import": { fg: RGBA.fromHex("#FF7B72"), bold: true },
"keyword.operator": { fg: RGBA.fromHex("#FF7B72") },
string: { fg: RGBA.fromHex("#A5D6FF") },
comment: { fg: RGBA.fromHex("#8B949E"), italic: true },
number: { fg: RGBA.fromHex("#79C0FF") },
boolean: { fg: RGBA.fromHex("#79C0FF") },
constant: { fg: RGBA.fromHex("#79C0FF") },
// Functions and types
function: { fg: RGBA.fromHex("#D2A8FF") },
"function.call": { fg: RGBA.fromHex("#D2A8FF") },
"function.method.call": { fg: RGBA.fromHex("#D2A8FF") },
type: { fg: RGBA.fromHex("#FFA657") },
constructor: { fg: RGBA.fromHex("#FFA657") },
// Variables and properties
variable: { fg: RGBA.fromHex("#E6EDF3") },
"variable.member": { fg: RGBA.fromHex("#79C0FF") },
property: { fg: RGBA.fromHex("#79C0FF") },
// Operators and punctuation
operator: { fg: RGBA.fromHex("#FF7B72") },
punctuation: { fg: RGBA.fromHex("#F0F6FC") },
"punctuation.bracket": { fg: RGBA.fromHex("#F0F6FC") },
"punctuation.delimiter": { fg: RGBA.fromHex("#C9D1D9") },
// Default fallback
default: { fg: RGBA.fromHex("#E6EDF3") },
})
### Style properties
Each style definition can include:
| Property | Type | Description |
| --- | --- | --- |
| `fg` | `RGBA` | Foreground (text) color |
| `bg` | `RGBA` | Background color |
| `bold` | `boolean` | Bold text |
| `italic` | `boolean` | Italic text |
| `underline` | `boolean` | Underlined text |
| `dim` | `boolean` | Dimmed text |
Supported languages
-------------------
Code uses Tree-sitter for parsing. Tree-sitter supports these languages:
* TypeScript / JavaScript
* Markdown
* Zig
* And any language with a Tree-sitter grammar
Streaming mode
--------------
Enable streaming mode when content arrives incrementally, like LLM output:
const code = new CodeRenderable(renderer, {
id: "streaming-code",
content: "",
filetype: "typescript",
syntaxStyle,
streaming: true, // Enable streaming mode
})
// Later, append content
code.content += "const x = 1\n"
code.content += "const y = 2\n"
Streaming mode optimizes highlighting for incremental updates.
Text selection
--------------
Enable text selection for copy operations:
const code = new CodeRenderable(renderer, {
id: "code",
content: sourceCode,
filetype: "typescript",
syntaxStyle,
selectable: true,
selectionBg: "#264F78",
selectionFg: "#FFFFFF",
})
Concealment
-----------
The `conceal` option controls whether certain syntax elements (like markdown formatting characters) are hidden:
const code = new CodeRenderable(renderer, {
id: "markdown",
content: "# Heading\n**bold** text",
filetype: "markdown",
syntaxStyle,
conceal: true, // Hide formatting characters
})
With line numbers
-----------------
Use `LineNumberRenderable` to add line numbers:
import { CodeRenderable, LineNumberRenderable, ScrollBoxRenderable } from "@opentui/core"
const code = new CodeRenderable(renderer, {
id: "code",
content: sourceCode,
filetype: "typescript",
syntaxStyle,
width: "100%",
})
const lineNumbers = new LineNumberRenderable(renderer, {
id: "code-with-lines",
target: code,
minWidth: 3,
paddingRight: 1,
fg: "#6b7280",
bg: "#161b22",
width: "100%",
})
// Wrap in ScrollBox for scrolling
const scrollbox = new ScrollBoxRenderable(renderer, {
id: "scrollbox",
width: 60,
height: 20,
})
scrollbox.add(lineNumbers)
Properties
----------
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `content` | `string` | `""` | Source code to display |
| `filetype` | `string` | \- | Language for syntax highlighting |
| `syntaxStyle` | `SyntaxStyle` | required | Syntax highlighting theme |
| `streaming` | `boolean` | `false` | Optimize for incremental content updates |
| `conceal` | `boolean` | `true` | Hide concealed syntax elements |
| `drawUnstyledText` | `boolean` | `true` | Show text before highlighting completes |
| `treeSitterClient` | `TreeSitterClient` | \- | Custom Tree-sitter client instance |
### Inherited from TextBufferRenderable
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `fg` | `string \| RGBA` | \- | Default foreground color |
| `bg` | `string \| RGBA` | \- | Background color |
| `selectable` | `boolean` | `false` | Enable text selection |
| `selectionBg` | `string \| RGBA` | \- | Selection background color |
| `selectionFg` | `string \| RGBA` | \- | Selection foreground color |
| `wrapMode` | `string` | `"word"` | Text wrapping: `"none"`, `"char"`, `"word"` |
| `tabIndicator` | `string \| number` | \- | Tab display character or width |
Additional properties
---------------------
| Property | Type | Description |
| --- | --- | --- |
| `lineCount` | `number` | Number of lines in content |
| `scrollY` | `number` | Current vertical scroll position (get/set) |
| `scrollX` | `number` | Current horizontal scroll position (get/set) |
| `scrollWidth` | `number` | Total scrollable width (read-only) |
| `scrollHeight` | `number` | Total scrollable height (read-only) |
| `isHighlighting` | `boolean` | Whether highlighting is in progress |
| `plainText` | `string` | Raw text content |
Markdown styles
---------------
For markdown highlighting, use markup-prefixed style names:
const markdownStyle = SyntaxStyle.fromStyles({
"markup.heading": { fg: RGBA.fromHex("#58A6FF"), bold: true },
"markup.heading.1": { fg: RGBA.fromHex("#00FF88"), bold: true, underline: true },
"markup.heading.2": { fg: RGBA.fromHex("#00D7FF"), bold: true },
"markup.bold": { fg: RGBA.fromHex("#F0F6FC"), bold: true },
"markup.strong": { fg: RGBA.fromHex("#F0F6FC"), bold: true },
"markup.italic": { fg: RGBA.fromHex("#F0F6FC"), italic: true },
"markup.list": { fg: RGBA.fromHex("#FF7B72") },
"markup.quote": { fg: RGBA.fromHex("#8B949E"), italic: true },
"markup.raw": { fg: RGBA.fromHex("#A5D6FF") },
"markup.raw.block": { fg: RGBA.fromHex("#A5D6FF") },
"markup.link": { fg: RGBA.fromHex("#58A6FF"), underline: true },
"markup.link.url": { fg: RGBA.fromHex("#58A6FF"), underline: true },
default: { fg: RGBA.fromHex("#E6EDF3") },
})
---
# Diff
Diff
====
Render unified or split diffs with syntax highlighting and optional line numbers.
Basic usage
-----------
### Renderable API
import { DiffRenderable, SyntaxStyle, RGBA, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const syntaxStyle = SyntaxStyle.fromStyles({
default: { fg: RGBA.fromHex("#E6EDF3") },
string: { fg: RGBA.fromHex("#A5D6FF") },
keyword: { fg: RGBA.fromHex("#FF7B72"), bold: true },
})
const diff = new DiffRenderable(renderer, {
id: "diff",
width: "100%",
height: 16,
diff: `diff --git a/app.ts b/app.ts\nindex 1111111..2222222 100644\n--- a/app.ts\n+++ b/app.ts\n@@ -1,3 +1,3 @@\n-const a = 1\n+const a = 2\n`,
view: "split",
filetype: "typescript",
syntaxStyle,
showLineNumbers: true,
})
renderer.root.add(diff)
Construct API
-------------
> Not available yet. Use `DiffRenderable` for now.
Properties
----------
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `diff` | `string` | `""` | Unified diff string |
| `view` | `"unified"` or `"split"` | `"unified"` | Layout style |
| `filetype` | `string` | \- | Syntax highlighting language |
| `syntaxStyle` | `SyntaxStyle` | \- | Syntax style for code |
| `wrapMode` | `"word"`, `"char"`, or `"none"` | \- | Code wrapping mode |
| `conceal` | `boolean` | `false` | Conceal markup when highlighting |
| `treeSitterClient` | `TreeSitterClient` | \- | Custom Tree-sitter client |
| `showLineNumbers` | `boolean` | `true` | Show line numbers |
| `lineNumberFg` | `string` or `RGBA` | `#888888` | Line number text color |
| `lineNumberBg` | `string` or `RGBA` | transparent | Line number background |
| `addedLineNumberBg` | `string` or `RGBA` | transparent | Line number background for added |
| `removedLineNumberBg` | `string` or `RGBA` | transparent | Line number background for removed |
| `addedBg` | `string` or `RGBA` | `#1a4d1a` | Background for added lines |
| `removedBg` | `string` or `RGBA` | `#4d1a1a` | Background for removed lines |
| `contextBg` | `string` or `RGBA` | transparent | Background for context lines |
| `addedContentBg` | `string` or `RGBA` | \- | Optional content background (added) |
| `removedContentBg` | `string` or `RGBA` | \- | Optional content background (removed) |
| `contextContentBg` | `string` or `RGBA` | \- | Optional content background (context) |
| `addedSignColor` | `string` or `RGBA` | `#22c55e` | Sign color for added lines |
| `removedSignColor` | `string` or `RGBA` | `#ef4444` | Sign color for removed lines |
---
# Line numbers
Line numbers
============
Add a line number gutter to renderables that provide line info, such as `CodeRenderable` and text editor components.
Basic usage
-----------
### Renderable API
import {
CodeRenderable,
LineNumberRenderable,
ScrollBoxRenderable,
SyntaxStyle,
RGBA,
createCliRenderer,
} from "@opentui/core"
const renderer = await createCliRenderer()
const syntaxStyle = SyntaxStyle.fromStyles({
default: { fg: RGBA.fromHex("#E6EDF3") },
})
const code = new CodeRenderable(renderer, {
id: "code",
content: "const x = 1\nconst y = 2\n",
filetype: "typescript",
syntaxStyle,
width: "100%",
})
const lineNumbers = new LineNumberRenderable(renderer, {
id: "code-lines",
target: code,
minWidth: 3,
paddingRight: 1,
fg: "#6b7280",
bg: "#161b22",
})
const scrollbox = new ScrollBoxRenderable(renderer, {
id: "scrollbox",
width: 70,
height: 18,
})
scrollbox.add(lineNumbers)
renderer.root.add(scrollbox)
Line signs and colors
---------------------
lineNumbers.setLineColor(3, "#2b6cb0")
lineNumbers.setLineSign(3, { before: ">", beforeColor: "#2b6cb0" })
Construct API
-------------
> Not available yet. Use `LineNumberRenderable` for now.
Properties
----------
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `target` | `Renderable & LineInfoProvider` | \- | Target renderable to number |
| `fg` | `string` or `RGBA` | `#888888` | Gutter text color |
| `bg` | `string` or `RGBA` | transparent | Gutter background color |
| `minWidth` | `number` | `3` | Minimum gutter width |
| `paddingRight` | `number` | `1` | Right padding for gutter |
| `lineColors` | `Map` | \- | Per-line background colors |
| `lineSigns` | `Map` | \- | Per-line signs (before/after) |
| `lineNumberOffset` | `number` | `0` | Offset for line numbering |
| `hideLineNumbers` | `Set` | \- | Lines to hide numbers for |
| `lineNumbers` | `Map` | \- | Override line numbers per line |
| `showLineNumbers` | `boolean` | `true` | Toggle gutter visibility |
Methods
-------
| Method | Description |
| --- | --- |
| `setLineColor()` | Set a background color for a line |
| `clearLineColor()` | Clear a line background color |
| `setLineSign()` | Set a sign before/after a line number |
| `clearLineSign()` | Clear a line sign |
| `setLineNumbers()` | Override multiple line numbers |
| `setHideLineNumbers()` | Hide line numbers for specific lines |
---
# Environment variables
Environment variables
=====================
OpenTUI reads environment variables at runtime. Bun loads `.env` automatically, so you can set these in your shell or in a `.env` file.
Variables
---------
| Variable | Type | Default | Description |
| --- | --- | --- | --- |
| `OTUI_TS_STYLE_WARN` | `string` | `false` | Enable warnings for missing syntax styles |
| `OTUI_TREE_SITTER_WORKER_PATH` | `string` | `""` | Path to the Tree-sitter worker |
| `XDG_CONFIG_HOME` | `string` | `""` | Base directory for user-specific configuration files |
| `XDG_DATA_HOME` | `string` | `""` | Base directory for user-specific data files |
| `OTUI_DEBUG_FFI` | `boolean` | `false` | Enable debug logging for the FFI bindings |
| `OTUI_SHOW_STATS` | `boolean` | `false` | Show the debug overlay at startup |
| `OTUI_TRACE_FFI` | `boolean` | `false` | Enable tracing for the FFI bindings |
| `OPENTUI_FORCE_WCWIDTH` | `boolean` | `false` | Use wcwidth for character width calculations |
| `OPENTUI_FORCE_UNICODE` | `boolean` | `false` | Force Mode 2026 Unicode support in terminal capabilities |
| `OPENTUI_GRAPHICS` | `boolean` | `true` | Enable Kitty graphics protocol detection |
| `OPENTUI_FORCE_NOZWJ` | `boolean` | `false` | Use no\_zwj width method (Unicode without ZWJ joining) |
| `OPENTUI_FORCE_EXPLICIT_WIDTH` | `string` | \- | Force explicit width detection (`true`/`1` or `false`/`0`) |
| `OTUI_USE_CONSOLE` | `boolean` | `true` | Enable global `console.*` capture for the built-in overlay |
| `SHOW_CONSOLE` | `boolean` | `false` | Open the built-in console overlay at startup |
| `OTUI_DUMP_CAPTURES` | `boolean` | `false` | Dump captured stdout and console caches from the exit handler |
| `OTUI_NO_NATIVE_RENDER` | `boolean` | `false` | Skip the Zig/native frame renderer |
| `OTUI_DEBUG` | `boolean` | `false` | Capture all raw stdin input for debugging |
Notes
-----
* `OPENTUI_FORCE_EXPLICIT_WIDTH=false` skips OSC 66 queries on older terminals.
* Disable global `console.*` capture with `OTUI_USE_CONSOLE=false`. `consoleMode` only changes the overlay surface.
* `OTUI_NO_NATIVE_RENDER` still runs the render loop. In `"split-footer"` mode, the current stdout flush path can still emit ANSI cursor movement and clear sequences.
* `OTUI_DUMP_CAPTURES` runs from the renderer exit handler. A direct `renderer.destroy()` call does not trigger it by itself.
---
# Solid.js
Solid.js bindings
=================
Build terminal user interfaces with Solid.js’s fine-grained reactivity and OpenTUI.
Installation
------------
bun install solid-js @opentui/solid
Setup
-----
### 1\. Configure TypeScript
Add JSX config to `tsconfig.json`:
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "@opentui/solid"
}
}
### 2\. Configure Bun
Add preload script to `bunfig.toml`:
preload = ["@opentui/solid/preload"]
### 3\. Enable runtime-loaded plugin support (if needed)
If your app loads external TS/TSX modules at runtime (for example a file-based plugin system), import this once in your entry file before dynamic imports:
import "@opentui/solid/runtime-plugin-support"
### 4\. Create your app
import { render } from "@opentui/solid"
const App = () => Hello, World!
render(App)
Run with `bun index.tsx`.
Components
----------
OpenTUI Solid provides JSX intrinsic elements that map to core renderables.
**Note:** Solid uses snake\_case for multi-word component names (e.g., `ascii_font`, `tab_select`).
### Layout & display
* `` - Styled text container
* `` - Layout container with borders
* `` - Scrollable container
* `` - ASCII art text
* `` - Render Markdown content
### Input
* `` - Single-line text input
* `