bento-layout

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-layout
import { 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 including safe variants, 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 as element.style. Results come back through the layout getter, 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

On this page