Renderables
A renderable is an imperative node in OpenTUI’s retained tree. It stores layout, visual state, children, event handlers, and native resources.
Retained means the same objects stay in the tree between frames. Change their properties instead of rebuilding them for each update.
Create and update#
Create a renderable with a render context. A CliRenderer implements that context.
import { BoxRenderable, RGBA, TextRenderable, createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer()
const foreground = RGBA.defaultForeground()
const fill = RGBA.fromIndex(243)
const panel = new BoxRenderable(renderer, {
id: "panel",
width: 18,
height: 3,
paddingX: 1,
border: true,
borderColor: foreground,
backgroundColor: fill,
})
const status = new TextRenderable(renderer, {
id: "status",
content: "Waiting",
fg: foreground,
})
panel.add(status)
renderer.root.add(panel)The shaded panel contains a text child:
Update the existing nodes. The wider panel also gives its text child more space:
status.content = "Ready"
panel.width = 30Setters such as content, width, visible, and zIndex request another render when their state changes.
Tree membership#
Each renderable has at most one parent. add() reparents an existing node when necessary.
const row = new BoxRenderable(renderer, { width: 34, flexDirection: "row", gap: 2 })
renderer.root.add(row)
const [first, second] = ["first", "second"].map((id) => {
const parent = new BoxRenderable(renderer, {
id,
title: id,
width: 16,
height: 3,
paddingX: 1,
border: true,
borderColor: foreground,
})
row.add(parent)
return parent
})
const message = new BoxRenderable(renderer, {
id: "message",
width: 10,
height: 1,
backgroundColor: fill,
})
message.add(new TextRenderable(renderer, { content: "Message", fg: foreground }))
first.add(message)Add the same subtree to second. You do not need to remove it from first:
second.add(message)
console.log(message.parent === second) // true
console.log(first.getChildrenCount()) // 0add(child, index) inserts at an index. insertBefore(child, anchor) inserts before a direct child. Both methods return the inserted index, or -1 when they cannot add the value.
Use these methods to inspect the tree:
getChildren()returns a new array of direct children.getChildrenCount()returns the direct-child count.getRenderable(id)finds a direct child.findDescendantById(id)searches descendants recursively.
Reparenting calls remove() on the old parent. As a result, onRemove() runs for a temporary detach and for a reparent.
Do not release final owned resources in onRemove(). Release them in destroySelf(), which runs only during destruction. See Lifecycle and cleanup.
Hide or detach#
Hiding a child keeps it in the tree. remove(child) detaches a direct child without destroying it.
Start with two children:
const panel = new BoxRenderable(renderer, {
width: 12,
height: 4,
border: true,
borderColor: foreground,
})
const detail = new BoxRenderable(renderer, { height: 1, backgroundColor: fill })
detail.add(new TextRenderable(renderer, { content: "Detail", fg: foreground }))
const next = new TextRenderable(renderer, { content: "Next", fg: foreground })
panel.add(detail)
panel.add(next)
renderer.root.add(panel)Both hiding and detaching move Next up. Only detaching changes tree membership:
detail.visible = false
console.log(panel.getChildrenCount(), detail.parent === panel) // 2, true
detail.visible = true
panel.remove(detail)
console.log(panel.getChildrenCount(), detail.parent) // 1, nullNeither operation destroys detail. Set visible = true to show a hidden child, or call add(detail) to reattach a detached child. Reattaching does not change its visibility.
visible = false sets the Yoga node to display: none. The node does not receive layout or render work while hidden. Hiding a focused node also blurs it.
Layout properties#
Renderables participate in the Yoga-based layout model. Their computed x, y, width, and height can change after layout or terminal resize.
zIndex changes sibling render and hit-test order without changing layout order. translateX and translateY move drawing and hit bounds without changing the Yoga result.
opacity applies to the node and its descendants. overflow: "hidden" or "scroll" clips rendering and mouse hit bounds to the node.
Shared mouse, focus, and selection behavior belongs to Interaction, focus, and selection. Keyboard routing belongs to Keyboard input.
Destruction#
destroy() is idempotent. It detaches the node, releases its frame buffer and Yoga node, removes listeners, and calls destroySelf().
destroy() detaches direct children but does not destroy them. Use destroyRecursively() when this node owns the complete subtree.
detail.destroyRecursively()
panel.destroyRecursively()The renderer destroys its root recursively during renderer cleanup. Destroy detached subtrees yourself when you no longer need them.
Do not add a destroyed renderable to another parent.
Subclass render hooks, measurement, buffering, and resource examples belong to Custom renderables.
Next#
Use the components overview to choose a built-in renderable.