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}`)
})
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"
}