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:

ClassDescription
BoxRenderableContainer with border, background, and layout
TextRenderableRead-only styled text display
InputRenderableSingle-line text input
TextareaRenderableMulti-line editable text
SelectRenderableDropdown/list selection
TabSelectRenderableHorizontal tab selection
ScrollBoxRenderableScrollable container
ScrollBarRenderableStandalone scroll bar control
CodeRenderableSyntax-highlighted code display
LineNumberRenderableLine number gutter for code/text views
DiffRenderableUnified or split diff viewer
ASCIIFontRenderableASCII art font display
FrameBufferRenderableRaw framebuffer for custom graphics
MarkdownRenderableMarkdown renderer
SliderRenderableNumeric 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 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")
})

Focused descendants

A renderable can also react to focus living inside one of its descendants through the hasFocusedDescendant flag. When any child has focus, every ancestor’s hasFocusedDescendant flips to true and OpenTUI marks them dirty so they repaint. BoxRenderable uses this to paint the focusedBorderColor whenever the box itself is focusable and any descendant currently owns focus:

const panel = new BoxRenderable(renderer, {
  id: "panel",
  focusable: true,
  borderColor: "#444",
  focusedBorderColor: "#00AAFF",
  flexDirection: "column",
})

const input = new InputRenderable(renderer, { id: "panel-input" })
panel.add(input)

input.focus() // panel.hasFocusedDescendant === true → border recolors

Custom renderables can read this._hasFocusedDescendant (protected) or renderable.hasFocusedDescendant (public) to add similar effects.

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()