These docs describe the main branch, including unreleased changes. Read the 0.6.0 docs.

Renderables

A renderable is an imperative node in OpenTUI’s retained tree. It holds children, event handlers, and native resources. Each visual renderable also has a native scene node that holds its layout and paint state.

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 = 30

Setters 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()) // 0

add(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, null

Neither 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 node’s Yoga display to none. A hidden node and its descendants take no layout space. Native code does not paint them or run their update and paint hooks. A registered onLifecyclePass still runs. 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 paint and hit-test order without changing layout order. A new child paints after its existing siblings that have the same zIndex. Siblings with the same zIndex keep their previous paint order, so restoring a changed zIndex does not restore the child’s original paint position. Moving a child to a new index in the same parent changes its layout position, not its paint 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, destroys its frame buffer, removes listeners, and calls destroySelf(). Then it destroys the scene node, which owns the Yoga node.

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 paint hooks, measurement, buffering, and resource examples belong to Custom renderables.

Next#

Use the components overview to choose a built-in renderable.

Changes0.6.0, 0.5.0
0.6.0
Added Renderable._invokeNativePaint, Renderable.abortConstruction, Renderable.addListener, Renderable.assertMutable, Renderable.assignOptions, Renderable.defineNativeIntegration and 28 more.
Changed Renderable._screenX, Renderable._screenY.
Removed Renderable._getVisibleChildren, Renderable._hasVisibleChildFilter, Renderable.canReuseRenderCommandList, Renderable.getLayoutNode, Renderable.getScissorRect, Renderable.render and 10 more.
0.5.0
Added RootRenderable.takeCurrentRenderable, RootRenderable.currentRenderable.