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 proportional fonts (canvas rendering, rich text), use the Pretext integration. For all 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()

Text Measurement Backends ​

The composable API supports pluggable text measurement. Choose the backend that matches your rendering target:

typescript
import { createFlexily, createBareFlexily, pipe, withMonospace, withTestMeasurer, withPretext } from "flexily"

// Terminal (default) -- 1 char = 1 cell
const termFlex = createFlexily()

// Proportional fonts -- word-wrap, hyphenation, variable-width glyphs
import pretext from "@chenglou/pretext"
const canvasFlex = pipe(createBareFlexily(), withPretext(pretext))

// Deterministic (tests/CI) -- fixed glyph widths, cross-platform identical
const testFlex = pipe(createBareFlexily(), withTestMeasurer())

See Text Measurement Plugins in the API reference for details on each backend.

Next Steps ​