Introduction
A flexbox, CSS Grid, and block layout engine in plain TypeScript — no WASM, no dependencies, no node lifecycle to manage.
bento-layout is a flexbox, CSS Grid, and block layout engine in plain
TypeScript. Style data goes in, pixel positions come out — and that is the
whole transaction. No WASM binary to load, no async initialization, no engine
instance to register nodes with, no free() or destroy() to remember:
nodes are ordinary JavaScript objects with ordinary lifetimes, and an
unreferenced subtree is just garbage collected. Zero runtime dependencies.
It computes geometry, full stop. It does not paint, own a DOM, or parse CSS strings — which makes it the layout half of a canvas renderer, a terminal UI, a PDF generator, or an SVG diagram, without dragging in the rest of a browser.
Try it
Every demo on this site is live: edit the source and the real engine re-lays
it out as you type. Try changing alignItems to 'flex-start', or giving
the middle node flexGrow: 2.
<Layout>
<Node style={{width: '440px', height: '160px', display: 'flex',
columnGap: '12px', alignItems: 'center'}}>
<Node style={{width: '100px', height: '100px'}} />
<Node style={{flexGrow: 1, height: '60px'}} />
<Node style={{width: '100px', height: '100px'}} />
</Node>
</Layout>Demos accept CSS-shaped strings ('100px', '1fr', minmax()) because the
playground ships a small CSS-ish parser. The library itself deliberately does
not: in your code, values are structured data — 100, '50%', { fr: 1 } —
so the engine's input stays unambiguous. The
style reference covers the whole
vocabulary.
Quick start
npm install bento-layoutimport { LayoutNode, computeLayout } from 'bento-layout';
const left = LayoutNode.make({ flexGrow: 1 });
const right = LayoutNode.make({ flexGrow: 1 });
const root = LayoutNode.make({ width: 400, height: 300 }, [left, right]);
computeLayout(root, { width: 'max-content', height: 'max-content' });
left.layout.size; // { width: 200, height: 300 }
right.layout.location; // { x: 200, y: 0 }That is the entire setup: import, build a tree, lay it out, read positions back. It runs the same in Node, a browser, a worker, or an edge runtime — there is no loader and no sync-vs-async API fork, because there is nothing to load.
What's in the box
- Three layout modes, one tree. Flexbox, CSS Grid, and block layout (with
margin collapsing per CSS 2.2) over the full box model: min/max constraints,
aspect ratios, percentages, auto margins, absolute positioning, RTL,
box-sizing, gaps, alignment includingsafevariants, and scrollbar gutters. The modes compose freely — a grid inside a flex row inside a block page is the normal case, not a special one. That is exactly what this is:
<Layout>
<Node style={{display: 'block', width: '440px', padding: '12px'}}>
<Node style={{height: '32px', marginBottom: '12px'}} />
<Node style={{display: 'flex', columnGap: '12px', height: '150px'}}>
<Node style={{width: '120px'}} />
<Node style={{flexGrow: 1, display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gridAutoRows: '69px', gap: '12px'}}>
<Node style={{}} />
<Node style={{}} />
<Node style={{}} />
<Node style={{}} />
<Node style={{}} />
<Node style={{}} />
</Node>
</Node>
</Node>
</Layout>- Plain data in, plain objects out.
LayoutNode.make(style, children)takes flat camelCase CSS properties, the same spelling aselement.style. Results come back through thelayoutgetter, snapped to whole pixels the way browsers round — or unrounded, your choice. - Content measures itself. Leaf nodes take a measure callback, so text and images report their own size. See Measuring content.
- Verified against a browser. Every conformance fixture is geometry extracted from a pinned Chrome — 5,304 fixtures at 0.1px tolerance, plus a differential fuzzer and a WPT scoreboard on the same oracle. How correctness is verified has the whole story.
Where next
- Getting started — a tutorial from install to a working ASCII renderer, in about fifteen minutes.
- Guides: Measuring content, Grid layouts, and Driving a renderer.
- Reference: the API, every style property, and what CSS is covered.
- Background: why plain TypeScript and how correctness is verified.