Headless flex layout engine for terminal UIs. Pure TypeScript, zero runtime dependencies.
Pilates is a flex layout engine designed for the terminal: integer cell coordinates, CJK / emoji / wide-char awareness, ANSI escape passthrough, and unbundled from any UI framework. Use it directly to compute layouts, or wrap the included renderer to produce styled strings.
import { render } from '@pilates/render'; process.stdout.write( render({ width: 80, height: 6, flexDirection: 'row', children: [ { flex: 1, border: 'rounded', title: 'Logs', children: [{ text: 'user logged in' }] }, { width: 20, border: 'single', title: 'Status', children: [{ text: 'ok', color: 'green', bold: true }] }, ], }), ); // โญโ Logs โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎโโ Status โโโโโโโโโโ // โuser logged in โโok โ // โ โโ โ // โ โโ โ // โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏโโโโโโโโโโโโโโโโโโโโ
Why
Terminal UIs in JavaScript are dominated by Ink, which couples two distinct concerns into one package: a WASM flex layout engine and a React reconciler. If you want the layout half, you have to take all of React. Pilates separates them:
@pilates/coreโ the engine. ImperativeNodeAPI, returns integer cell coordinates. Pure TypeScript, zero runtime dependencies. Handles CJK / emoji / wide-char widths, integer-cell rounding, the CSS Flexbox freeze loop, and absolute positioning. Validated cell-for-cell against a reference WASM flexbox implementation across 33 oracle fixtures.@pilates/renderโ the out-of-box renderer. Declarative POJO tree โ painted ANSI string with borders, titles, colors, and text wrap. Uses core internally; depends only on it.@pilates/diffโ cell-level frame diffing + minimal ANSI redraw sequences for live TUIs. Pairs with@pilates/render.@pilates/reactโ optional React reconciler on top of the same engine, for consumers who want JSX and hooks. Independent of the core / render / diff stack โ you don't pay for it if you don't import it.@pilates/widgetsโ interactive widgets (TextInput,Select,Spinner) built on@pilates/react. For wizard-style CLI flows.
Packages
| Package | Status | What |
|---|---|---|
@pilates/core |
2.0.1 |
Engine: imperative Node API, returns layout boxes. |
@pilates/render |
1.0.2 |
Out-of-box: declarative tree โ painted string. |
@pilates/diff |
0.2.1 |
Cell-level frame diff + minimal ANSI redraw. |
@pilates/react |
0.4.1 |
React reconciler โ author terminal UIs with JSX, hooks, mouse, focus, scroll. |
@pilates/widgets |
0.1.0-rc.4 |
Interactive widgets (TextInput, Select, Spinner, MultiSelect, Tabs, Table, ProgressBar, TextArea) for @pilates/react. |
Examples
Eleven runnable examples live under examples/ โ six built
on the imperative @pilates/render API, five built on @pilates/react.
Imperative (@pilates/render):
| Example | What it shows |
|---|---|
| chat-log | Two-pane chat layout: scrolling messages + status sidebar. Wide-char & emoji passthrough. |
| dashboard | System-monitor layout: status header, four stat tiles in a row, metrics strip. |
| gallery | Grid of cards that wraps to multiple rows on a narrow container. |
| modal | Confirm-action modal floating over a list โ exercises absolute positioning. |
| progress-table | Multi-row progress dashboard with bars and color-coded status. |
| split-pane | Editor-style: header + 3-pane body (files / editor / outline) + status footer. |
React (@pilates/react + @pilates/widgets):
| Example | What it shows |
|---|---|
| react-build-dashboard | Flagship demo. Interactive build-pipeline dashboard: <ScrollView> ร 2, mouse, useFocus, keyboard nav, animation, <ProgressBar> + <Spinner> widgets, all stitched together. |
| react-counter | Minimal reconciler example: counter incrementing every 250ms, demonstrating the diff-based redraw loop. |
| react-dashboard | React port of dashboard with a live tick counter on the header. |
| react-modal | React port of modal: centered confirmation dialog over a scrollable list. |
| react-wizard | Multi-step TextInput โ Select โ Spinner wizard exercising every @pilates/widgets component. |
pnpm install # imperative pnpm --filter @pilates-examples/chat-log dev pnpm --filter @pilates-examples/progress-table dev # react pnpm --filter @pilates-examples/react-counter dev pnpm --filter @pilates-examples/react-wizard dev # flagship pnpm --filter @pilates-examples/react-build-dashboard dev
Quick start (using just the engine)
import { Node, Edge } from '@pilates/core'; const root = Node.create(); root.setFlexDirection('row'); root.setWidth(80); root.setHeight(24); root.setPadding(Edge.All, 1); const main = Node.create(); main.setFlex(1); const sidebar = Node.create(); sidebar.setWidth(20); root.insertChild(main, 0); root.insertChild(sidebar, 1); root.calculateLayout(); main.getComputedLayout(); // { left:1, top:1, width:58, height:22 } sidebar.getComputedLayout(); // { left:59, top:1, width:20, height:22 }
You'd then paint to the terminal yourself โ or pass the same shape via the
declarative API to @pilates/render to skip the painting:
import { render } from '@pilates/render'; process.stdout.write( render({ width: 80, height: 24, flexDirection: 'row', padding: 1, children: [{ flex: 1 }, { width: 20 }], }), );
What's supported
| Category | Properties |
|---|---|
| Direction | flexDirection (row / column / -reverse), flexWrap (nowrap / wrap / wrap-reverse) |
| Sizing | width, height, minWidth, minHeight, maxWidth, maxHeight |
| Flex | flex (shorthand), flexGrow, flexShrink, flexBasis |
| Spacing | padding / margin per edge, gap (row + column) |
| Alignment | justifyContent, alignItems, alignSelf, alignContent (all CSS values) |
| Position | positionType (relative / absolute), position per edge |
| Visibility | display (flex / none) |
| Render-only | border (5 styles), borderColor, title, color, bgColor, bold, italic, underline, dim, inverse, wrap |
Out of v1: aspectRatio, RTL/LTR direction inheritance, baseline alignment,
input handling, animations, scroll containers, style inheritance.
Performance
Pure-TypeScript layout, validated cell-for-cell against WASM Yoga.
Across the 9 scenarios in our bench suite, the pure-TS engine is
faster than WASM Yoga on each โ including the structural-mutation
workload (append + remove a row per frame) Yoga led on through
mid-2026. Numbers are median latency from pnpm bench (Node 22,
win32-x64, ~5s tinybench window with bootstrap CI95; a hand-picked
suite, not a universal claim โ real workloads will differ):
| Scenario | Pilates core | yoga-layout (WASM) | Pilates speedup |
|---|---|---|---|
| tiny (10 nodes) | 4.5ยตs | 19.0ยตs | 4.2ร faster |
| realistic (~100) | 121ยตs | 328ยตs | 2.7ร faster |
| stress (~1000) | 601ยตs | 1.94ms | 3.2ร faster |
| big (~5000) | 3.32ms | 9.17ms | 2.8ร faster |
| huge (~10000) | 8.62ms | 18.5ms | 2.1ร faster |
| hot-relayout (1k persistent, mutate one leaf/frame) | 16.3ยตs | 83.0ยตs | 5.1ร faster |
| hot-relayout + boundaries (same + explicit-sized rows) | 15.8ยตs | 77.8ยตs | 4.9ร faster |
| hot-relayout (text mutation, fixed-size table) | 8.9ยตs | 90.6ยตs | 10ร faster |
| hot-structural (append + remove a row / frame) | 71.3ยตs | 118.3ยตs | 1.7ร faster |
The hot-relayout and hot-structural patterns โ building a tree once and mutating-and-relaying out per frame โ are the workloads Yoga's WASM compute advantage traditionally won on. The Spineless incremental layout engine (an attribute-grammar dependency graph + priority-queue recomputation; refined through phases 8โ17 with a typed-array runtime, linear-recurrence main-axis positions, and fold-default input elimination) flips that: a single leaf mutation re-evaluates only the fields actually downstream of the change, and structural mutations patch only the affected subtree.
For trees of pure fixed-size cells (e.g. a data table with one cell's
text length changing per frame), the direct @pilates/core (spineless)
runtime mutation goes through in ~0.2ยตs โ 380ร faster than the
Yoga round-trip. That path is @internal for now; the public
calculateLayout ships the engine and is what every other Pilates
consumer uses.
WASM Yoga's compute kernel is genuinely fast in isolation, but every
setProperty / Node.create crosses the JSโWASM boundary; that
marshalling cost dominates at TUI tree sizes (10โ10k nodes), and the
Spineless engine's incremental recompute then beats WASM's per-frame
full layout. Pure-TS Pilates pays no marshalling cost.
Reproduce with pnpm bench. Full numbers + scenario shapes in
bench/RESULTS.md.
Validation
Every flex feature is verified cell-for-cell against a reference WASM flexbox implementation:
- 33 oracle fixtures (fixed widths, flex distributions, padding, margin,
gap, min/max, all
justifyContent/alignItems/alignSelf/alignContentvalues,flexWrap,flexWrap: wrap-reverse, every absolute positioning anchor) - 200+ unit + algorithm + render tests
- Unicode width fuzzer running through 200 randomized strings against
@xterm/headlessper CI run, plus a fixture set of pinned agreement cases and documented divergences (where modern terminals render wider than xterm.js's Unicode-11 tables) - Property-based fuzz with
fast-checkover layout invariants โ non-overflow, sibling non-overlap, reproducibility โ across randomly generated trees
Notable design choices
- Default
flexShrink: 0in core (React Native convention, not CSS's 1) โ declared widths stay declared. The render layer flips this to 1 for text leaves so wrapped text fits its container. - Absolute offsets are relative to the parent's outer box, not its content (post-padding) box โ React Native semantics, not CSS. Keeps consumers porting from Ink / RN consistent.
- Integer cell rounding rounds absolute corners and derives size from
rounded edges โ sibling boxes butt cleanly across uneven splits
(
[100, flex:1, flex:1, flex:1]โ[34, 33, 33]).
Status
@pilates/core@2.0.1 is on npm. Core algorithm + flex pipeline are
feature-complete, validated cell-for-cell against WASM Yoga, and faster
than Yoga on each of the 9 scenarios in the bench suite (see Performance
above) โ powered by the Spineless incremental engine. The React layer
ships mouse, scroll, focus management, typed errors, and layout devtools.
Contributing
Issues, discussions, and PRs welcome. Start with CONTRIBUTING.md for setup, the test loop, and what the maintainer expects from layout-algorithm changes (oracle-fixture coverage). By participating you agree to follow the Code of Conduct. Security issues: see SECURITY.md for the private disclosure channel.
License
MIT ยฉ Zhijie Wang.





















