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 real code, run by the real engine as you type —
with the hover docs, completions, and type checking of a proper editor. This
one is the holy grail layout:
header, footer, a fluid centre column, and two fixed side columns. Drag the
viewport's right edge and watch it reflow — the root has no width, so it
takes whatever space it is laid into, exactly as a page does.
import { LayoutNode } from 'bento-layout';
const header = LayoutNode.make({ height: 48 });
const nav = LayoutNode.make({ width: 120 });
const main = LayoutNode.make({ flexGrow: 1, minWidth: 0 });
const aside = LayoutNode.make({ width: 96 });
const footer = LayoutNode.make({ height: 36 });
const body = LayoutNode.make(
{ flexGrow: 1, columnGap: 12 },
[nav, main, aside],
);
const page = LayoutNode.make(
{
flexDirection: 'column',
height: 260,
padding: 12,
rowGap: 12,
},
[header, body, footer],
);
renderPlayground(page);Everything on this page is one computeLayout call — no CSS engine, no DOM.
Try giving aside a flexGrow: 1, or swapping nav's width for
{ min: 100, max: 160 }-style constraints via minWidth/maxWidth.
What you see is exactly what runs in your project: copy a demo out, swap
renderPlayground(root) for computeLayout(root, …) plus your renderer, and
it works as-is. Values are structured data — 100, '50%', { fr: 1 } —
never CSS strings, 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. This is a media-object card wall: a block page, a flex row for the sidebar, and a grid of cards that reflows as the viewport narrows.
import { LayoutNode } from 'bento-layout';
const cards = Array.from({ length: 6 }, () =>
LayoutNode.make({}),
);
const wall = LayoutNode.make(
{
display: 'grid',
flexGrow: 1,
minWidth: 0,
gridTemplateColumns: [
{
repeat: 'auto-fill',
tracks: [{ min: 100, max: { fr: 1 } }],
},
],
gridAutoRows: [{ min: 69, max: 69 }],
gap: 12,
},
cards,
);
const root = LayoutNode.make(
{ display: 'block', padding: 12 },
[
LayoutNode.make({ height: 32, marginBottom: 12 }),
LayoutNode.make({ display: 'flex', columnGap: 12 }, [
LayoutNode.make({ width: 120 }),
wall,
]),
],
);
renderPlayground(root);- 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.