A Declarative Box-Split and Collision Engine for Vector Graphics. It provides a CSS-like layout grammar (padding, gap, cols, rows, auto-height) and resolves it into a clean JSON coordinate tree for any visual canvas (SVG, <canvas>, WebGL).
Unlike typical layout libraries (like Yoga or standard web browsers) that hide resolved position values inside a black box, Boxwood exposes absolute coordinate geometry ({x, y, w, h}) directly. That makes it easy to draw responsive visuals, route connectors, and manage collision-safe compositions.
npm install @canwork/boxwoodimport { resolveLayout, defaultMeasure, LNode } from "@canwork/boxwood";
const layoutTree: LNode = {
style: { padding: 20 },
split: { cols: ["60%", "40%"], gap: 15 },
children: [
{ id: "main-panel", style: { w: "100%", h: "100%" } },
{
style: { w: "100%", h: "100%" },
split: { rows: 2, gap: 10 },
children: [
{ id: "sub-card-1", style: { padding: 12 } },
{ id: "sub-card-2", style: { padding: 12 } },
],
},
],
};
const boundary = { x: 0, y: 0, w: 1920, h: 1080 };
const result = resolveLayout(layoutTree, boundary, { measure: defaultMeasure });
console.log(result.boxes);<script src="https://cdn.jsdelivr.net/npm/@canwork/boxwood/dist/boxwood.global.js"></script>
<script>
const { resolveLayout, defaultMeasure } = Boxwood;
const result = resolveLayout(layoutTree, boundary, { measure: defaultMeasure });
</script>Or via unpkg:
<script src="https://unpkg.com/@canwork/boxwood/dist/boxwood.global.js"></script>flowchart TD
A["LNode tree\nstyle / split / children"] --> B["resolveLayout(root, boundary, opts)"]
B --> C["Resolve layout rules\npadding, gap, cols, rows, grid"]
B --> D["Measure content\ntext wrap / overflow"]
C --> E["Compute contentFrame and box geometry"]
D --> E
E --> F["Resolved boxes\n{x, y, w, h}"]
F --> G["Render with SVG / Canvas / WebGL"]
flowchart TB
R["root LNode"] --> C1["child LNode"]
R --> C2["child LNode"]
C1 --> G1["grandchild LNode"]
C2 --> G2["grandchild LNode"]
C1 --> G3["grandchild LNode"]
C2 --> G4["grandchild LNode"]
G1 --> B1["resolved box"]
G2 --> B2["resolved box"]
G3 --> B3["resolved box"]
G4 --> B4["resolved box"]
Boxwood is designed for developers who need a declarative layout model but also need the exact coordinates to render shapes, arrows, labels, and collision-aware content.
| Layout Library | Declarative Box Model | Coordinates for Lines/Arrows | Lightweight & Zero-DOM | Collision Separation |
|---|---|---|---|---|
| Yoga / Flexbox | ✅ Yes | ❌ No | ❌ No | |
| D3 / Cytoscape | ❌ No | ✅ Yes | ✅ Yes | |
| Boxwood | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
- Declarative layout rules for cols, rows, grids, gap, padding, margin, and auto sizing
- Absolute coordinate output ready for SVG,
<canvas>, WebGL, or custom rendering - Nested layout composition with resolved geometry for every node
- Text measurement and overflow-aware sizing via
measure - Collision resolution so layout boxes do not overlap unexpectedly
Open examples/interactive-demo.html in your browser to see the engine running with a live resize demo.
Layouts are represented as a nested tree structure:
import { LNode } from "@canwork/boxwood";
const layoutTree: LNode = {
style: { padding: 20 },
split: { cols: ["60%", "40%"], gap: 15 },
children: [
{
id: "main-panel",
style: { w: "100%", h: "100%" }
},
{
style: { w: "100%", h: "100%" },
split: { rows: 2, gap: 10 },
children: [
{ id: "sub-card-1", style: { padding: 12 } },
{ id: "sub-card-2", style: { padding: 12 } }
]
}
]
};Run the solver on your layout tree by passing the root node and the parent boundary dimensions:
import { resolveLayout } from "@canwork/boxwood";
const boundary = { x: 0, y: 0, w: 1920, h: 1080 }; // 1080p frame
const result = resolveLayout(layoutTree, boundary);
console.log(result.boxes);
/*
Outputs a flat list of absolute coordinate boxes:
[
{ id: "main-panel", box: { x: 20, y: 20, w: 1128, h: 1040 } },
{ id: "sub-card-1", box: { x: 1163, y: 20, w: 737, h: 515 } },
{ id: "sub-card-2", box: { x: 1163, y: 545, w: 737, h: 515 } }
]
*/Because Boxwood returns absolute coordinate values, routing arrows and lines between elements is simple:
// Find the resolved boxes
const boxA = result.boxes.find(b => b.node.id === "sub-card-1")?.box;
const boxB = result.boxes.find(b => b.node.id === "sub-card-2")?.box;
if (boxA && boxB) {
// Center coordinates of both boxes
const x1 = boxA.x + boxA.w / 2;
const y1 = boxA.y + boxA.h / 2;
const x2 = boxB.x + boxB.w / 2;
const y2 = boxB.y + boxB.h / 2;
// Render SVG or Canvas lines using these points
// e.g. <line x1={x1} y1={y1} x2={x2} y2={y2} stroke="gold" />
}You can pass custom measurement rules to child nodes using a measure hook. The engine will automatically evaluate if text fits inside a container. If text wraps or overflows, it raises a structured OverflowSignal in the solver result, letting you decrease font size or handle resizing programmatically.
When visual cards enter with scale spring animations, or dynamic data changes their sizes on the fly, Boxwood runs an overlap solver to push boxes apart, guaranteeing your nodes never overlap.
examples/showcase.html — open it directly in any browser. It loads Boxwood from a CDN and runs one complete use case: a declarative pipeline dashboard that re-resolves on every resize. Drag the ◢ corner and the same tree reflows from columns to rows while the SVG connectors re-route to follow the boxes. The entire integration is the drawScene() function at the bottom of the file — read it to see exactly how you'd wire Boxwood into your own project: build a tree → resolveLayout() → draw the boxes → connect their coordinates.
Each scene is a real *.scene.ts module — the way you'd actually structure Boxwood inside a TypeScript app. Run the build to bundle them into interactive, resizable HTML pages:
npm run examples # → writes examples/01..04 *.htmlThen open any of the generated pages and drag the corner handle:
01-bento-grid.scene.ts: A dashboard "Bento" grid built from nested col/row splits, with a hero tile and a KPI cluster. Resize and it reflows from a 3-column layout to a single-column stack; every tile auto-sizes to its wrapped copy.02-vector-connections.scene.ts: A 1-source / 3-worker / 1-sink pipeline. The six bezier connectors are routed purely from the resolved box coordinates, so they stay correct whether the split is laid out as columns (wide) or rows (tall).03-absolute-to-responsive.scene.ts: Takes a PDF extract's absolutex/widthspans, converts them to percentage columns once, and lets the engine reflow them — the two-column body narrows and the paragraphs rewrap as you shrink the panel.04-dynamic-reflow-overflow.scene.ts: Auto-height (a card with no fixed height grows to its text), a realOverflowSignal(a fixed-height card surfaces a banner once the text no longer fits), and collision separation (two badges placed at the same coordinate are nudged apart).
The shared helpers live in examples/kit.ts (connectors, intrinsic sizing) and examples/shell.ts (the resize runtime + SVG drawing). The original brutalist playground is still available at examples/interactive-demo.html.
MIT © Canwork Studios
