Skip to content

Getting Started with Flexily

Building a terminal UI? Use silvery, which uses Flexily by default. You get React components, hooks, and layout feedback without touching the low-level API below.

This guide is for developers who need the Flexily API directly — framework authors, canvas renderers, or anyone replacing Yoga.

Installation

bash
npm install flexily
bash
bun add flexily
bash
pnpm add flexily
bash
yarn add flexily

The composable API is the recommended way to use Flexily. It provides a batteries-included engine with pluggable text measurement:

typescript
import { createFlexily, FLEX_DIRECTION_ROW } from "flexily"

const flex = createFlexily()

// Create nodes with built-in text support
const root = flex.createNode()
root.setWidth(80)
root.setFlexDirection(FLEX_DIRECTION_ROW)

const label = flex.createNode()
label.setTextContent("Hello world")
root.insertChild(label, 0)

const content = flex.createNode()
content.setFlexGrow(1)
root.insertChild(content, 1)

flex.calculateLayout(root, 80, 24)

console.log(label.getComputedWidth()) // 11 (one cell per character)
console.log(content.getComputedWidth()) // 69

createFlexily() includes monospace text measurement by default (1 char = 1 cell), making it ideal for terminal UIs. For custom text backends, see Composable API in the API reference.

Low-Level API

The low-level API provides direct Yoga-compatible access. Use it when you need full control or are migrating from Yoga:

Your First Layout

Create a simple row with two children:

typescript
import { Node, FLEX_DIRECTION_ROW, DIRECTION_LTR } from "flexily"

// Create the root node
const root = Node.create()
root.setWidth(200)
root.setHeight(100)
root.setFlexDirection(FLEX_DIRECTION_ROW)

// Add two children that split the space
const left = Node.create()
left.setFlexGrow(1)
root.insertChild(left, 0)

const right = Node.create()
right.setFlexGrow(1)
root.insertChild(right, 1)

// Calculate layout
root.calculateLayout(200, 100, DIRECTION_LTR)

// Read results
console.log(left.getComputedWidth()) // 100
console.log(right.getComputedWidth()) // 100
console.log(left.getComputedLeft()) // 0
console.log(right.getComputedLeft()) // 100

// Clean up
root.free()

Adding Spacing

Use padding, margin, and gap for spacing:

typescript
import { Node, FLEX_DIRECTION_ROW, EDGE_ALL, GUTTER_COLUMN, DIRECTION_LTR } from "flexily"

const root = Node.create()
root.setWidth(200)
root.setFlexDirection(FLEX_DIRECTION_ROW)
root.setPadding(EDGE_ALL, 10) // 10px padding on all sides
root.setGap(GUTTER_COLUMN, 8) // 8px gap between columns

const a = Node.create()
a.setFlexGrow(1)
root.insertChild(a, 0)

const b = Node.create()
b.setFlexGrow(1)
root.insertChild(b, 1)

root.calculateLayout(200, undefined, DIRECTION_LTR)

// Children are inside padding, separated by gap
console.log(a.getComputedLeft()) // 10
console.log(a.getComputedWidth()) // 86  (200 - 20 padding - 8 gap) / 2
console.log(b.getComputedLeft()) // 104 (10 + 86 + 8)

Nested Layouts

Build complex layouts by nesting nodes:

typescript
import { Node, FLEX_DIRECTION_ROW, FLEX_DIRECTION_COLUMN, DIRECTION_LTR } from "flexily"

const root = Node.create()
root.setWidth(300)
root.setHeight(200)
root.setFlexDirection(FLEX_DIRECTION_ROW)

// Sidebar (fixed width)
const sidebar = Node.create()
sidebar.setWidth(80)
root.insertChild(sidebar, 0)

// Main content (fills remaining space)
const main = Node.create()
main.setFlexGrow(1)
main.setFlexDirection(FLEX_DIRECTION_COLUMN)
root.insertChild(main, 1)

// Header (fixed height)
const header = Node.create()
header.setHeight(30)
main.insertChild(header, 0)

// Body (fills remaining space)
const body = Node.create()
body.setFlexGrow(1)
main.insertChild(body, 1)

root.calculateLayout(300, 200, DIRECTION_LTR)

console.log(sidebar.getComputedWidth()) // 80
console.log(main.getComputedWidth()) // 220
console.log(header.getComputedHeight()) // 30
console.log(body.getComputedHeight()) // 170

Measure Functions

Tip: If you use the composable API (createFlexily()), you can call node.setTextContent("text") instead of writing a manual measure function. The engine handles text measurement automatically.

For nodes that size based on content (like text), use measure functions:

typescript
import { Node, FLEX_DIRECTION_ROW, MEASURE_MODE_EXACTLY, DIRECTION_LTR } from "flexily"

const root = Node.create()
root.setWidth(200)
root.setFlexDirection(FLEX_DIRECTION_ROW)

const textNode = Node.create()
textNode.setMeasureFunc((width, widthMode, height, heightMode) => {
  const text = "Hello, World!"
  const charWidth = 8 // assume 8px per character
  const lineHeight = 16

  if (widthMode === MEASURE_MODE_EXACTLY) {
    // Wrap text to fit exact width
    const charsPerLine = Math.floor(width / charWidth)
    const lines = Math.ceil(text.length / charsPerLine)
    return { width, height: lines * lineHeight }
  }

  // Measure natural size
  return { width: text.length * charWidth, height: lineHeight }
})

root.insertChild(textNode, 0)
root.calculateLayout(200, undefined, DIRECTION_LTR)

Dirty Tracking

Flexily tracks which nodes have changed, so relayout only recalculates what's needed:

typescript
root.calculateLayout(200, 100, DIRECTION_LTR) // Full calculation

// Modify one node
left.setWidth(50)
console.log(left.isDirty()) // true

root.calculateLayout(200, 100, DIRECTION_LTR) // Only recalculates dirty subtree

Cleanup

Always free nodes when done:

typescript
// Free entire tree
root.free()

// Or free individual nodes (must remove children first)
root.removeChild(child)
child.free()
root.free()

Next Steps