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 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);
574 × 0 px

Nothing to show yet.

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-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. 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);
574 × 0 px

Nothing to show yet.

  • 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