Grid layouts
How to express CSS Grid templates, placement, and implicit tracks as structured data.
CSS Grid in this library is the same grid you know from the browser; only
the spelling differs. There is no CSS string parser, so a track list is an
array of structured values rather than 'repeat(3, 1fr)'. This guide maps
the common grid patterns onto that vocabulary.
Every demo on this page is live and uses that same structured vocabulary — what you edit here is exactly what your code passes.
A minimal grid
Set display: 'grid' and give the container column tracks. Items flow into
cells automatically, row by row:
import { LayoutNode, computeLayout } from 'bento-layout';
const fr1 = { min: 'auto', max: { fr: 1 } } as const;
const cells = Array.from({ length: 6 }, () => LayoutNode.make());
const grid = LayoutNode.make(
{
display: 'grid',
width: 320,
gridTemplateColumns: [fr1, fr1, fr1],
gridAutoRows: [{ min: 40, max: 40 }],
gap: 10,
},
cells,
);
computeLayout(grid, { width: 'max-content', height: 'max-content' });
cells[0].layout.size; // { width: 100, height: 40 }
cells[3].layout.location; // { x: 0, y: 50 } — second row
grid.layout.size; // { width: 320, height: 90 }Every track is a { min, max } pair — CSS minmax() with both halves
always stated. The single-value CSS forms are shorthands for pairs, and the
two you will use constantly:
| CSS | Structured |
|---|---|
100px | { min: 100, max: 100 } |
25% | { min: '25%', max: '25%' } |
1fr | { min: 'auto', max: { fr: 1 } } |
auto | { min: 'auto', max: 'auto' } |
minmax(100px, 1fr) | { min: 100, max: { fr: 1 } } |
fit-content(200px) | { min: 'auto', max: { fitContent: 200 } } |
Note that bare 1fr takes an 'auto' minimum — that is CSS's own rule
(1fr means minmax(auto, 1fr)), and it is why a 1fr track refuses to
shrink below its content unless you write { min: 0, max: { fr: 1 } },
the equivalent of minmax(0, 1fr).
Equal columns with repeat
A repeat() is an object in the template array, expanding in place:
const grid = LayoutNode.make(
{
display: 'grid',
width: 300,
height: 50,
gridTemplateColumns: [{ repeat: 3, tracks: [{ min: 'auto', max: { fr: 1 } }] }],
},
[LayoutNode.make(), LayoutNode.make(), LayoutNode.make()],
);
computeLayout(grid, { width: 'max-content', height: 'max-content' });
grid.children[2].layout.location; // { x: 200, y: 0 }A sidebar layout
Fixed sidebar, flexible content with a floor — CSS's
200px minmax(100px, 1fr). Drag the viewport below: the sidebar holds at
200px while the content column takes every pixel of the difference, until
the minmax floor stops it at 100px and the grid overflows rather than
crushing the content.
import { LayoutNode } from 'bento-layout';
const page = LayoutNode.make(
{
display: 'grid',
width: '100%',
height: 180,
gridTemplateColumns: [
{ min: 200, max: 200 },
{ min: 100, max: { fr: 1 } },
],
columnGap: 16,
},
[LayoutNode.make({}), LayoutNode.make({})],
);
renderPlayground(page);As many cards as fit
auto-fill fits as many repetitions as the container allows:
{ repeat: 'auto-fill', tracks: [{ min: 150, max: { fr: 1 } }] } is CSS's
repeat(auto-fill, minmax(150px, 1fr)). It needs a definite width to fill —
an auto-sized grid shrink-wraps and produces a single repetition.
Laid into 520px, three 150px-minimum tracks fit and the leftover stretches
them to 167px each, with the fourth card wrapping to a second row at
{ x: 0, y: 90 }. (167, not 166.67 — layout is pixel-snapped by default,
cumulatively, so the three tracks come out 167/166/167 and stay flush. See
Driving a renderer for the rounding
rules.)
'auto-fit' is the same, but repetitions left with no items collapse to
zero and their space goes back to the filled tracks.
This is the responsive card wall in its natural habitat: the grid below is
width: '100%', so it takes whatever the viewport gives it. Drag the
viewport's right edge and watch the track count change on its own — at 460px
a third 150px track no longer fits beside the gaps, so the grid drops to two
columns, then to one. That is the whole responsive card grid, with no media
queries and nothing to recompute by hand.
import { LayoutNode } from 'bento-layout';
const cards = Array.from({ length: 5 }, () =>
LayoutNode.make({}),
);
const wall = LayoutNode.make(
{
display: 'grid',
width: '100%',
gridTemplateColumns: [
{
repeat: 'auto-fill',
tracks: [{ min: 150, max: { fr: 1 } }],
},
],
gridAutoRows: [{ min: 80, max: 80 }],
gap: 10,
},
cards,
);
renderPlayground(wall);Placing items
Placement is per axis, on the item: gridColumnStart / gridColumnEnd and
gridRowStart / gridRowEnd. Each end is 'auto', a line, or a span.
Lines are 1-based and negative lines count from the end, so { line: -1 }
is the last line — the idiom for "stretch to the end":
const banner = LayoutNode.make({
gridColumnStart: { line: 1 },
gridColumnEnd: { line: -1 }, // full width
});
const tall = LayoutNode.make({ gridRowEnd: { span: 2 } }); // two rowsIn a three-column, 300px grid with 40px rows, the banner comes out
{ width: 300, height: 40 } spanning every column, and tall is
{ width: 100, height: 80 } at { x: 0, y: 40 } — two rows, tucked under
the banner. The unplaced items flow around the placed ones.
gridAutoFlow controls the direction and packing: 'row' (the default)
fills each row before moving down, 'column' fills columns first, and the
-dense variants backtrack to fill holes at the cost of source order.
A placement CSS would reject — line 0, which does not exist — falls back
to 'auto' instead of throwing, matching how browsers recover.
import { LayoutNode } from 'bento-layout';
const banner = LayoutNode.make({
gridColumnStart: { line: 1 },
gridColumnEnd: { line: -1 },
});
const tall = LayoutNode.make({ gridRowEnd: { span: 2 } });
const grid = LayoutNode.make(
{
display: 'grid',
width: '100%',
gridTemplateColumns: [
{
repeat: 3,
tracks: [{ min: 'auto', max: { fr: 1 } }],
},
],
gridAutoRows: [{ min: 40, max: 40 }],
gap: 6,
},
[
banner,
tall,
LayoutNode.make({}),
LayoutNode.make({}),
LayoutNode.make({}),
],
);
renderPlayground(grid);Implicit tracks
Items placed beyond the explicit template create implicit tracks. Their
sizes come from gridAutoRows / gridAutoColumns, cycled in order —
[{ min: 40, max: 40 }, { min: 80, max: 80 }] alternates 40 and 80. With
neither set, implicit tracks are auto and size to their content.
What grid does not include
Named lines, grid-template-areas, and subgrid are not implemented — the
supported surface is the unnamed-track model above, chosen so that
everything claimed is browser-verified. The full list lives in
CSS coverage. Alignment inside cells
(justifyItems, alignItems, and the per-item justifySelf /
alignSelf) works as in CSS and shares the flexbox vocabulary — see the
style reference.