The composable API is the recommended way to use Flexily. It provides a high-level engine with pluggable text measurement, built on top of the low-level Node API.
Create a batteries-included engine with monospace text measurement.
import { createFlexily } from "flexily"
const flex = createFlexily()
const node = flex.createNode()
node.setTextContent("Hello world")
flex.calculateLayout(node, 80, 24)Options:
| Option | Type | Default | Description |
|---|---|---|---|
charWidth |
number |
1 |
Width of each character cell |
charHeight |
number |
1 |
Height of each character cell |
defaults |
"yoga" | "css" |
"yoga" |
Style-defaults preset for created nodes — see Yoga divergences |
For terminal UIs, the default charWidth=1, charHeight=1 maps directly to terminal cells.
Create a minimal engine with no text measurement plugin. Use pipe() to add plugins. Accepts the same optional defaults preset as createFlexily().
import { createBareFlexily, pipe, withTestMeasurer } from "flexily"
const flex = pipe(createBareFlexily(), withTestMeasurer())Calling setTextContent() on a bare engine without a text plugin throws an error.
Apply plugins to an engine, left to right. Returns the modified engine.
import { createBareFlexily, pipe, withMonospace, withTestMeasurer } from "flexily"
const flex = pipe(createBareFlexily(), withMonospace(1, 1))The composable engine interface.
interface FlexilyEngine {
createNode(): FlexilyNode
calculateLayout(root: FlexilyNode, width?: number, height?: number, direction?: number): void
textLayout?: TextLayoutService
}| Method | Description |
|---|---|
createNode() |
Create a new FlexilyNode (Node with text content mixin) |
calculateLayout |
Calculate layout for a tree (direction defaults to LTR) |
textLayout |
The active text measurement backend (if any) |
Extends Node with text content methods. Created via engine.createNode().
interface FlexilyNode extends Node {
setTextContent(text: string, style?: Partial<ResolvedTextStyle>): void
getTextContent(): string | null
}| Method | Description |
|---|---|
setTextContent() |
Set text content and install an automatic measure function |
getTextContent() |
Get the current text content (or null if none) |
Calling setMeasureFunc() on a FlexilyNode clears its text content. Calling setTextContent() replaces any existing measure function.
A function that extends or configures the engine.
type FlexilyPlugin = (engine: FlexilyEngine) => FlexilyEngineThe pluggable text measurement backend interface.
interface TextLayoutService {
prepare(input: TextPrepareInput): PreparedText
}prepare() takes text and style, returning a PreparedText object that can compute intrinsic sizes and layout at any width.
Monospace text measurement. Each grapheme cluster = charWidth units wide, always 1 line (no wrapping). Default for createFlexily().
import { createBareFlexily, pipe, withMonospace } from "flexily"
const flex = pipe(createBareFlexily(), withMonospace(1, 1))| Parameter | Type | Default | Description |
|---|---|---|---|
charWidth |
number |
1 |
Width per character cell |
charHeight |
number |
1 |
Height per character cell |
Deterministic text measurement for tests and CI. Fixed grapheme width table: Latin 0.8, CJK 1.0, emoji 1.8 (relative to fontSize). Supports word wrapping.
import { createBareFlexily, pipe, withTestMeasurer } from "flexily"
const flex = pipe(createBareFlexily(), withTestMeasurer())Cross-platform deterministic -- same results on every OS and runtime.
Proportional font measurement via @chenglou/pretext (peer dependency). For Canvas-based text rendering.
import { createBareFlexily, pipe, withPretext } from "flexily"
import pretext from "@chenglou/pretext"
const flex = pipe(createBareFlexily(), withPretext(pretext))| Parameter | Type | Description |
|---|---|---|
pretext |
PretextAPI |
The pretext module (install separately) |
The low-level Yoga-compatible API. All composable API nodes extend Node.
import { Node } from "flexily"
const node = Node.create()Nodes form a tree. Each node represents a flexbox container or leaf.
node.insertChild(child, index) // Insert child at index
node.removeChild(child) // Remove child
node.getChildCount() // Number of children
node.getChild(index) // Get child by index
node.getParent() // Get parent node (or null)node.setWidth(100) // Fixed width
node.setHeight(50) // Fixed height
node.setWidthAuto() // Auto width
node.setHeightAuto() // Auto height
node.setWidthPercent(50) // 50% of parent
node.setHeightPercent(50) // 50% of parent
node.setMinWidth(10)
node.setMinHeight(10)
node.setMaxWidth(200)
node.setMaxHeight(200)import {
FLEX_DIRECTION_ROW,
FLEX_DIRECTION_COLUMN,
FLEX_DIRECTION_ROW_REVERSE,
FLEX_DIRECTION_COLUMN_REVERSE,
WRAP_WRAP,
WRAP_NO_WRAP,
JUSTIFY_CENTER,
JUSTIFY_FLEX_START,
JUSTIFY_FLEX_END,
JUSTIFY_SPACE_BETWEEN,
JUSTIFY_SPACE_AROUND,
JUSTIFY_SPACE_EVENLY,
ALIGN_CENTER,
ALIGN_FLEX_START,
ALIGN_FLEX_END,
ALIGN_STRETCH,
ALIGN_BASELINE,
} from "flexily"
node.setFlexDirection(FLEX_DIRECTION_ROW)
node.setFlexWrap(WRAP_WRAP)
node.setFlexGrow(1)
node.setFlexShrink(0)
node.setFlexBasis(100)
node.setJustifyContent(JUSTIFY_CENTER)
node.setAlignItems(ALIGN_CENTER)
node.setAlignSelf(ALIGN_FLEX_END)
node.setAlignContent(ALIGN_CENTER)import {
EDGE_TOP,
EDGE_RIGHT,
EDGE_BOTTOM,
EDGE_LEFT,
EDGE_ALL,
EDGE_START,
EDGE_END,
GUTTER_ALL,
GUTTER_ROW,
GUTTER_COLUMN,
} from "flexily"
// Padding
node.setPadding(EDGE_ALL, 10)
node.setPadding(EDGE_TOP, 5)
// Margin
node.setMargin(EDGE_ALL, 10)
node.setMarginAuto(EDGE_LEFT)
// Border
node.setBorder(EDGE_ALL, 1)
// Gap
node.setGap(GUTTER_ALL, 10)
node.setGap(GUTTER_ROW, 5)
node.setGap(GUTTER_COLUMN, 5)The production engine accepts number | Value in all four spacing setters.
Numbers remain point lengths. Use parseLength with your adapter's ch and lh
scale to construct a Value; the existing resolver evaluates math expressions.
import { parseLength } from "flexily"
const scale = { ch: 1, lh: 1 } // Terminal cells; choose your adapter's scale.
node.setPadding(EDGE_LEFT, parseLength("2ch", scale))
node.setMargin(EDGE_START, parseLength("10cqi", scale))
node.setBorder(EDGE_LEFT, parseLength("calc(1ch + 2cqi)", scale))
node.setGap(GUTTER_ROW, parseLength("1lh", scale))cqi uses the nearest ancestor query container, skipping the node itself. A
query container's own padding, margin, border and gap therefore use its ancestor
context; its children use its frozen inline size. The layout root uses the width
passed to calculateLayout for its own CQ spacing. Resize resolves these values
again during layout and intrinsic measurement.
Inline edges (left, right, start and end) and column gaps refuse lh. Block edges
(top and bottom) and row gaps refuse ch and cqi. EDGE_ALL and GUTTER_ALL
check both axes before changing any value. Percentage padding and margins retain
their existing containing-block width basis; border widths refuse percentages,
including percentages inside math expressions.
getPadding and getMargin return stored Value objects. getBorder and
getGap retain numeric point results and throw LengthError for a stored
non-point length; read node.style.border or node.style.gap to inspect that
Value. After layout, getComputedPadding, getComputedMargin and
getComputedBorder resolve edge lengths in the layout context. There is no
computed gap getter. Unset logical borders still return NaN from getBorder.
The classic engine retains its numeric setters and percentage/auto behavior.
It refuses CQ, adapter units and math expressions in shared spacing storage with
a typed property error saying production engine only.
import { POSITION_TYPE_ABSOLUTE, POSITION_TYPE_RELATIVE } from "flexily"
node.setPositionType(POSITION_TYPE_ABSOLUTE)
node.setPosition(EDGE_TOP, 10)
node.setPosition(EDGE_LEFT, 20)node.setAspectRatio(16 / 9)For leaf nodes that measure their own content (e.g., text):
node.setMeasureFunc((width, widthMode, height, heightMode) => {
// widthMode: MEASURE_MODE_EXACTLY | MEASURE_MODE_AT_MOST | MEASURE_MODE_UNDEFINED
return { width: computedWidth, height: computedHeight }
})node.setBaselineFunc((width, height) => {
return baselineOffset // number
})import { DIRECTION_LTR, DIRECTION_RTL } from "flexily"
node.calculateLayout(availableWidth, availableHeight, DIRECTION_LTR)After calculateLayout():
node.getComputedLeft()
node.getComputedTop()
node.getComputedRight()
node.getComputedBottom()
node.getComputedWidth()
node.getComputedHeight()
node.getComputedPadding(EDGE_TOP)
node.getComputedMargin(EDGE_LEFT)
node.getComputedBorder(EDGE_ALL)node.isDirty() // Has this node been modified since last layout?
node.markDirty() // Force recalculationnode.free() // Free this node and clean up referencesAll constants are Yoga-compatible. Import from flexily:
| Category | Constants |
|---|---|
| Direction | DIRECTION_LTR, DIRECTION_RTL, DIRECTION_INHERIT |
| Flex Direction | FLEX_DIRECTION_ROW, FLEX_DIRECTION_COLUMN, FLEX_DIRECTION_ROW_REVERSE, FLEX_DIRECTION_COLUMN_REVERSE |
| Justify | JUSTIFY_FLEX_START, JUSTIFY_CENTER, JUSTIFY_FLEX_END, JUSTIFY_SPACE_BETWEEN, JUSTIFY_SPACE_AROUND, JUSTIFY_SPACE_EVENLY |
| Align | ALIGN_FLEX_START, ALIGN_CENTER, ALIGN_FLEX_END, ALIGN_STRETCH, ALIGN_BASELINE, ALIGN_AUTO, ALIGN_SPACE_BETWEEN, ALIGN_SPACE_AROUND, ALIGN_SPACE_EVENLY |
| Wrap | WRAP_NO_WRAP, WRAP_WRAP, WRAP_WRAP_REVERSE |
| Position | POSITION_TYPE_STATIC, POSITION_TYPE_RELATIVE, POSITION_TYPE_ABSOLUTE |
| Edge | EDGE_LEFT, EDGE_TOP, EDGE_RIGHT, EDGE_BOTTOM, EDGE_START, EDGE_END, EDGE_HORIZONTAL, EDGE_VERTICAL, EDGE_ALL |
| Gap | GUTTER_COLUMN, GUTTER_ROW, GUTTER_ALL |
| Measure Mode | MEASURE_MODE_EXACTLY, MEASURE_MODE_AT_MOST, MEASURE_MODE_UNDEFINED |
| Overflow | OVERFLOW_VISIBLE, OVERFLOW_HIDDEN, OVERFLOW_SCROLL |
| Display | DISPLAY_FLEX, DISPLAY_NONE |
Flexily exports diagnostic helpers for downstream consumers:
import {
getLayout,
formatLayout,
diffLayouts,
textMeasure,
assertLayoutSanity,
expectRelayoutMatchesFresh,
expectIdempotent,
expectResizeRoundTrip,
} from "flexily/testing"| Export | Description |
|---|---|
getLayout(node) |
Recursively extract computed layout as plain objects |
formatLayout(layout) |
Pretty-print a layout tree for debugging |
diffLayouts(a, b) |
Node-by-node diff with NaN-safe comparison |
textMeasure(width) |
Factory for wrapping text measure functions |
assertLayoutSanity(node) |
Validate dimensions are finite and non-negative |
expectRelayoutMatchesFresh(buildTree, w, h) |
Differential oracle: incremental must match fresh |
expectIdempotent(buildTree, w, h) |
Two identical layouts must produce same result |
expectResizeRoundTrip(buildTree, widths) |
Resize sequence must match fresh at final width |