Skip to main content

Overview

OpenTUI uses the Yoga layout engine, which implements a subset of CSS Flexbox. This provides:
  • Responsive layouts that adapt to terminal size
  • Flexible sizing with grow/shrink behavior
  • Alignment and justification controls
  • Row and column layouts
  • Nested layout containers
Yoga was created by Facebook (now Meta) and is the same layout engine used in React Native.
Yoga is implemented in native code (Zig) for high performance, but you interact with it through TypeScript.

Flexbox Basics

Flex Direction

Control whether children stack horizontally or vertically:
import { GroupRenderable } from "@opentui/core"

const row = new GroupRenderable(renderer, {
  flexDirection: "row",        // Horizontal layout →
  // or "column" (default)     // Vertical layout ↓
  // or "row-reverse"          // Reverse horizontal ←
  // or "column-reverse"       // Reverse vertical ↑
})

Justify Content

Align children along the main axis (direction of flex flow):
const container = new GroupRenderable(renderer, {
  flexDirection: "row",
  justifyContent: "space-between",
  // Options:
  // "flex-start"    - Pack at start (default)
  // "flex-end"      - Pack at end
  // "center"        - Center items
  // "space-between" - Space between items
  // "space-around"  - Space around items
  // "space-evenly"  - Equal spacing
})

Align Items

Align children along the cross axis (perpendicular to flex flow):
const container = new GroupRenderable(renderer, {
  flexDirection: "row",
  alignItems: "center",
  // Options:
  // "flex-start"  - Align to start (default)
  // "flex-end"    - Align to end
  // "center"      - Center items
  // "baseline"    - Align baselines
  // "stretch"     - Stretch to fill
})

Align Self

Override alignItems for a specific child:
const child = new BoxRenderable(renderer, {
  alignSelf: "flex-end",  // This child aligns to end, others follow parent
})

Sizing

Fixed Sizes

const box = new BoxRenderable(renderer, {
  width: 50,   // Fixed width: 50 cells
  height: 20,  // Fixed height: 20 cells
})

Auto Sizing

const box = new BoxRenderable(renderer, {
  width: "auto",   // Size to fit content
  height: "auto",
})

Percentage Sizing

const box = new BoxRenderable(renderer, {
  width: "50%",   // 50% of parent width
  height: "25%",  // 25% of parent height
})

Min/Max Constraints

const box = new BoxRenderable(renderer, {
  minWidth: 20,
  maxWidth: 100,
  minHeight: 10,
  maxHeight: "50%",
})

Flex Properties

Flex Grow

Control how much a child grows to fill available space:
const sidebar = new BoxRenderable(renderer, {
  width: 20,      // Fixed width
  flexGrow: 0,    // Don't grow (default)
})

const content = new BoxRenderable(renderer, {
  flexGrow: 1,    // Grow to fill remaining space
})
Multiple children with flexGrow share space proportionally:
const left = new BoxRenderable(renderer, {
  flexGrow: 1,    // Gets 1/3 of available space
})

const center = new BoxRenderable(renderer, {
  flexGrow: 2,    // Gets 2/3 of available space
})

const right = new BoxRenderable(renderer, {
  flexGrow: 1,    // Gets 1/3 of available space
})

Flex Shrink

Control how much a child shrinks when space is limited:
const important = new BoxRenderable(renderer, {
  width: 50,
  flexShrink: 0,  // Never shrink below 50
})

const flexible = new BoxRenderable(renderer, {
  width: 50,
  flexShrink: 1,  // Can shrink if needed (default)
})
By default, elements with explicit width/height have flexShrink: 0, while auto-sized elements have flexShrink: 1.

Flex Basis

Set the initial size before growing/shrinking:
const box = new BoxRenderable(renderer, {
  flexBasis: 30,     // Start at 30 cells
  flexGrow: 1,       // Then grow to fill space
})

Flex Wrap

Control whether children wrap to new lines:
const container = new GroupRenderable(renderer, {
  flexDirection: "row",
  flexWrap: "wrap",
  // Options:
  // "nowrap" - Single line (default)
  // "wrap"   - Wrap to new lines
  // "wrap-reverse" - Wrap in reverse
})

Spacing

Margin

Add space outside an element:
const box = new BoxRenderable(renderer, {
  margin: 2,           // All sides
  marginX: 3,          // Horizontal (left + right)
  marginY: 1,          // Vertical (top + bottom)
  marginTop: 1,        // Individual sides
  marginRight: 2,
  marginBottom: 1,
  marginLeft: 2,
})
Margins support:
  • Fixed numbers: margin: 5
  • Auto centering: marginLeft: "auto"
  • Percentages: margin: "10%"

Padding

Add space inside an element:
const box = new BoxRenderable(renderer, {
  padding: 2,          // All sides
  paddingX: 3,         // Horizontal
  paddingY: 1,         // Vertical
  paddingTop: 1,       // Individual sides
  paddingRight: 2,
  paddingBottom: 1,
  paddingLeft: 2,
})
Padding supports:
  • Fixed numbers: padding: 5
  • Percentages: padding: "5%"

Common Layouts

Horizontal Split

Two columns with fixed sidebar:
import { GroupRenderable, BoxRenderable } from "@opentui/core"

const container = new GroupRenderable(renderer, {
  flexDirection: "row",
  width: "100%",
  height: "100%",
})

const sidebar = new BoxRenderable(renderer, {
  width: 20,          // Fixed width
  backgroundColor: "#333",
})

const content = new BoxRenderable(renderer, {
  flexGrow: 1,        // Fill remaining space
  backgroundColor: "#111",
})

container.add(sidebar)
container.add(content)

Vertical Split

Header, content, footer:
const container = new GroupRenderable(renderer, {
  flexDirection: "column",
  width: "100%",
  height: "100%",
})

const header = new BoxRenderable(renderer, {
  height: 3,          // Fixed height
  backgroundColor: "#444",
})

const content = new BoxRenderable(renderer, {
  flexGrow: 1,        // Fill remaining space
  backgroundColor: "#111",
})

const footer = new BoxRenderable(renderer, {
  height: 1,          // Fixed height
  backgroundColor: "#444",
})

container.add(header)
container.add(content)
container.add(footer)

Centered Content

const container = new GroupRenderable(renderer, {
  width: "100%",
  height: "100%",
  justifyContent: "center",  // Center vertically (column direction)
  alignItems: "center",      // Center horizontally
})

const dialog = new BoxRenderable(renderer, {
  width: 40,
  height: 15,
  border: true,
})

container.add(dialog)

Space Between

Evenly distribute children:
const toolbar = new GroupRenderable(renderer, {
  flexDirection: "row",
  justifyContent: "space-between",
  width: "100%",
})

toolbar.add(new TextRenderable(renderer, { content: "File" }))
toolbar.add(new TextRenderable(renderer, { content: "Edit" }))
toolbar.add(new TextRenderable(renderer, { content: "View" }))
// Items will be evenly spaced: File <---> Edit <---> View

Responsive Grid

const grid = new GroupRenderable(renderer, {
  flexDirection: "row",
  flexWrap: "wrap",
  width: "100%",
})

for (let i = 0; i < 12; i++) {
  grid.add(new BoxRenderable(renderer, {
    width: "25%",  // 4 columns
    height: 5,
    margin: 1,
  }))
}

Position Types

Relative Positioning

Default - element participates in flexbox layout:
const box = new BoxRenderable(renderer, {
  position: "relative",  // Default
})

Absolute Positioning

Remove element from flex flow and position manually:
const overlay = new BoxRenderable(renderer, {
  position: "absolute",
  top: 0,
  right: 0,
  width: 30,
  height: 10,
  zIndex: 100,  // Render on top
})
Absolute positioning:
  • Doesn’t affect sibling layout
  • Positioned relative to parent
  • Requires explicit size and position

Overflow

Control how content beyond bounds is handled:
const scrollable = new BoxRenderable(renderer, {
  overflow: "scroll",   // Enable scrolling (clips + allows scroll)
  // or "hidden"        // Clip content
  // or "visible"       // Show all content (default)
  width: 50,
  height: 20,
})
When overflow: "scroll" or overflow: "hidden", children are clipped to the parent’s bounds.

Dynamic Layout Updates

Layout is automatically recalculated when:
  • Children are added/removed
  • Size properties change
  • Flex properties change
  • Terminal is resized
// This triggers layout recalculation
box.width = 100
box.flexGrow = 1
container.add(newChild)
Access computed layout after recalculation:
box.on("resize", () => {
  console.log(`New size: ${box.width}x${box.height}`)
})

Layout Performance

Minimize layout thrashing - Batch property changes when possible rather than updating one at a time.
Use absolute positioning sparingly - Absolute elements still participate in layout calculations but don’t affect siblings.
Yoga layout calculation is fast, but deeply nested trees with many children can impact performance. Profile if you notice slowness.

Layout Options Reference

interface LayoutOptions {
  // Flex container
  flexDirection?: "row" | "column" | "row-reverse" | "column-reverse"
  flexWrap?: "nowrap" | "wrap" | "wrap-reverse"
  justifyContent?: "flex-start" | "flex-end" | "center" | "space-between" | "space-around" | "space-evenly"
  alignItems?: "flex-start" | "flex-end" | "center" | "baseline" | "stretch"

  // Flex child
  flexGrow?: number
  flexShrink?: number
  flexBasis?: number | "auto"
  alignSelf?: "auto" | "flex-start" | "flex-end" | "center" | "baseline" | "stretch"

  // Position
  position?: "relative" | "absolute"
  top?: number | "auto" | `${number}%`
  right?: number | "auto" | `${number}%`
  bottom?: number | "auto" | `${number}%`
  left?: number | "auto" | `${number}%`

  // Size
  width?: number | "auto" | `${number}%`
  height?: number | "auto" | `${number}%`
  minWidth?: number | `${number}%`
  minHeight?: number | `${number}%`
  maxWidth?: number | `${number}%`
  maxHeight?: number | `${number}%`

  // Spacing
  margin?: number | "auto" | `${number}%`
  marginX?: number | "auto" | `${number}%`
  marginY?: number | "auto" | `${number}%`
  marginTop?: number | "auto" | `${number}%`
  marginRight?: number | "auto" | `${number}%`
  marginBottom?: number | "auto" | `${number}%`
  marginLeft?: number | "auto" | `${number}%`

  padding?: number | `${number}%`
  paddingX?: number | `${number}%`
  paddingY?: number | `${number}%`
  paddingTop?: number | `${number}%`
  paddingRight?: number | `${number}%`
  paddingBottom?: number | `${number}%`
  paddingLeft?: number | `${number}%`

  // Overflow
  overflow?: "visible" | "hidden" | "scroll"
}

Build docs developers (and LLMs) love