Documentation
Concepts

Model and engine

The layout lives in a typed model every change goes through as a command; an engine binds one grid to the page and runs the gestures.

The model

The model holds the grid's data and rules: the items, the columns, how the layout settles and how collisions are handled. Every change is a command run through a chain of middleware, and every committed layout is valid: in bounds, settled, and never overlapping unless you allow it.

model.ts
const { model } = useGridLayout();

model.run("item.move", { itemId: "revenue", x: 4, y: 0 });
model.run("item.resize", { itemId: "revenue", w: 6, h: 3, side: "bottom-end" });
model.can("item.remove", { itemId: "banner" }); // a dry run: would it apply?
model.get("item-by", { itemId: "revenue" }); // { id, x, y, w, h, … }

A command never throws on bad input: it returns { ok: true, value } or { ok: false, error: { code, message } }, with codes like not_found, refused (a static item), collision (under preventCollision) and vetoed.

commandwhat it does
layout.setreplaces the layout, corrected and settled
item.addadds an item at its cell, or the first free one
item.removeremoves an item
item.movemoves an item, pushing what it lands on
item.resizeresizes an item from a side, the opposite edge staying put
item.placemoves and resizes at once (the keyboard's drop)
item.configurechanges an item's limits and flags
grid.configurechanges the columns, rows, compaction and collision rules
breakpoint.setmakes a breakpoint the active one (responsive grids)
layouts.setreplaces every breakpoint's layout
layouts.generategives a breakpoint without a layout one, from another's

Middleware

A middleware sees every command, from a drag, a key or your own code. It can refuse it, rewrite its payload, or observe its result.

locked-zone.ts
model.use((ctx, next) => {
    if (ctx.command === "item.move" && ctx.payload.y < 1) return veto("the first row is the banner's");
    return next();
});

A gesture checks the model while it runs: a landing a middleware would refuse shows the item going back where it was, and the drop changes nothing. The engine passes its pixels to each check and each command it runs ({ env }, the third argument of run, can and check), for the constraints that need them; the model never keeps them.

The engine

An engine is one grid on screen. It measures the root, turns the layout into pixels, runs the pointer and keyboard gestures, and previews where the held item would land. A gesture ends in one command, or none.

engine.get("gesture") reads the gesture in progress, engine.get("cell-at", { clientX, clientY }) the cell under a point, and engine.run("cancel-gesture") ends one. useGridLayoutEvents hears each step of a gesture, for callbacks and announcements.

Messy layout
Gallery

Outside the root

useGridLayout() reads the grid inside its root. Elsewhere on the page (a toolbar, a sidebar), give the root a gridLayoutRef and read the grid through it: its current is { model, engine } while the root is mounted, and null before.

toolbar.tsx
const gridLayoutRef = useGridLayoutRef();

<Toolbar onClear={() => gridLayoutRef.current?.model.run("layout.set", { layout: [] })} />
<GridLayout.Root gridLayoutRef={gridLayoutRef}>…</GridLayout.Root>

useGridLayout(gridLayoutRef) follows it from any component, and renders again when a root takes or releases it.