@bndynet/layout
v0.1.0
Published
Lightweight, headless, framework-agnostic layout library with grid, flex and absolute engines.
Maintainers
Readme
@bndynet/layout
Lightweight, headless, framework-agnostic layout library.
One unified layout schema, three layout engines (grid, flex, absolute), computed into
plain style objects you can apply with any framework or vanilla DOM. No React, no Vue, no
react-grid-layout, no drag-and-drop dependencies — just a tiny, type-safe core.
- Unified schema & API, different engines under the hood.
- Headless: engines are pure functions returning
containerStyle/itemStyles/rects. - Framework-agnostic: ships ESM, CJS and IIFE; use it in React, Vue, Svelte, or plain HTML.
- Responsive breakpoints with id-based override merging.
- Nested layouts (a layout inside a layout item).
- Type-safe: full TypeScript types, no
anyin the public API. - Extensible: register custom engines via the registry.
What problem does it solve?
UIs mix different positioning models: a widget grid, flex toolbars/regions, and
absolute overlays or free canvases. Most libraries lock you into one model and one UI framework.
@bndynet/layout gives you a single declarative schema and computes the geometry/styles
for whichever engine an item needs, while staying completely decoupled from any rendering framework.
Install
pnpm add @bndynet/layout
# or: npm install @bndynet/layoutVia CDN (IIFE global Layout):
<script src="https://unpkg.com/@bndynet/layout"></script>
<script>
const { mountLayout } = Layout;
</script>Quick start (plain HTML)
import { mountLayout, type GridLayout } from '@bndynet/layout';
const layout: GridLayout = {
type: 'grid',
cols: 12,
rowHeight: 40,
gap: 12,
items: [
{ id: 'revenue', x: 0, y: 0, w: 6, h: 4 },
{ id: 'traffic', x: 6, y: 0, w: 6, h: 4 },
{ id: 'table', x: 0, y: 4, w: 12, h: 8 },
],
};
const el = document.getElementById('app')!;
mountLayout(el, {
layout,
content: {
revenue: 'Revenue',
traffic: 'Traffic',
table: (item) => {
const node = document.createElement('div');
node.textContent = `Widget: ${item.id}`;
return node;
},
},
});Content resolution order, per item: item.layout (nested) → content[id] → renderItem(item) →
nothing. Strings are inserted as text (never HTML) to avoid injection.
Grid
const grid: GridLayout = {
type: 'grid',
cols: 12, // default 12
rowHeight: 32, // default 32
gap: 12, // number or [x, y]
padding: 8,
items: [{ id: 'a', x: 0, y: 0, w: 6, h: 4 }],
};Container is position: relative; items are position: absolute, placed with
transform: translate(left, top). colWidth = (width - padding*2 - gapX*(cols-1)) / cols. The
rects map returns the pixel geometry of every visible item.
Flex
const flex: FlexLayout = {
type: 'flex',
direction: 'row', // 'row' | 'column'
wrap: false,
align: 'stretch', // start | center | end | stretch
justify: 'between', // start | center | end | between | around
gap: 12,
items: [
{ id: 'a', grow: 1, basis: 140 },
{ id: 'b', grow: 2 },
],
};Absolute
const absolute: AbsoluteLayout = {
type: 'absolute',
coordinate: 'px', // 'px' | 'percent'
items: [
{ id: 'tl', x: 16, y: 16, w: 160, h: 80 },
{ id: 'center', x: '50%', y: '50%', w: 160, h: 90, anchor: 'center' },
{ id: 'br', x: 16, y: 16, w: 140, h: 70, anchor: 'bottom-right' },
],
};With coordinate: 'percent', numeric values become percentages (50 → "50%"); string values pass
through untouched. anchor: 'center' adds translate(-50%, -50%); anchor: 'bottom-right' offsets
from right / bottom.
Responsive
const layout: GridLayout = {
type: 'grid',
cols: 12,
items: [
{ id: 'revenue', x: 0, y: 0, w: 6, h: 4 },
{ id: 'traffic', x: 6, y: 0, w: 6, h: 4 },
{ id: 'table', x: 0, y: 4, w: 12, h: 8 },
],
responsive: {
breakpoints: { lg: 1200, md: 900, sm: 600, xs: 0 },
layouts: {
sm: {
cols: 4,
items: [
{ id: 'revenue', x: 0, y: 0, w: 4, h: 4 },
{ id: 'traffic', x: 0, y: 4, w: 4, h: 4 },
{ id: 'table', x: 0, y: 8, w: 4, h: 8 },
],
},
},
},
};resolveBreakpoint(width, breakpoints) picks the largest threshold <= width (e.g. 1000 → md).
Overrides are merged onto the base layout, and items are merged by id — fields from the
override replace base fields; items without an override are preserved.
If you omit responsive.breakpoints, the library falls back to DEFAULT_BREAKPOINTS, so you only
need to key your layouts by the standard names:
import { DEFAULT_BREAKPOINTS } from '@bndynet/layout';
// { xs: 0, sm: 600, md: 900, lg: 1200, xl: 1536 }
const layout: GridLayout = {
type: 'grid',
cols: 12,
items: [/* ... */],
responsive: {
// no `breakpoints` needed — DEFAULT_BREAKPOINTS is used
layouts: {
sm: { cols: 4, items: [/* ... */] },
},
},
};Nested layouts
const layout: GridLayout = {
type: 'grid',
cols: 12,
items: [
{
id: 'section',
x: 0, y: 0, w: 12, h: 4,
layout: {
type: 'flex',
direction: 'row',
items: [
{ id: 'a', grow: 1 },
{ id: 'b', grow: 1 },
],
},
},
],
};When an item has a layout, the renderer mounts it recursively.
Headless usage
Compute styles directly and apply them however you like:
import { computeLayout } from '@bndynet/layout';
const { containerStyle, itemStyles, rects } = computeLayout(layout, { width: 1200 });Or use the controller for a live container (wires up ResizeObserver, SSR-safe):
import { createLayoutController } from '@bndynet/layout';
const controller = createLayoutController(el, layout, {
onChange: (result) => {
/* re-render with result.containerStyle / result.itemStyles */
},
});
controller.getContainerProps(); // { style }
controller.getItemProps('revenue'); // { style }
controller.update(nextLayout);
controller.setContext({ breakpoint: 'sm' });
controller.destroy();These getters return { style } objects, ready to spread onto framework elements (React
style={...}, Vue :style, etc.).
Custom engines
import { createEngineRegistry, type LayoutEngine } from '@bndynet/layout';
const myEngine: LayoutEngine = {
type: 'masonry',
normalize: (layout) => layout,
compute: (layout, ctx) => ({ containerStyle: {}, itemStyles: {} }),
};
const registry = createEngineRegistry([myEngine]);
computeLayout(layout, { width: 1200 }, { registry });Unknown layout types throw a clear error, e.g.
Unknown layout type: "masonry". Registered layout types are: "grid", "flex", "absolute".
SSR
computeLayout, engines, and resolve* helpers are pure and SSR-safe. createLayoutController
guards window/ResizeObserver, so it won't crash on the server (it simply doesn't observe).
mountLayout requires a DOM — call it on the client.
Roadmap
Not in v1, but the schema and engine interface reserve room for:
- Drag (the
lockfield is reserved for this). - Resize (
minW/minH/maxW/maxHare already part of the schema). - Collision with
pushcompaction (the gridcollision/compactfields are reserved). - Layout persistence (serialize/restore the schema).
- Custom engine plugins (already supported via the registry).
Build outputs
| Format | File |
| ------ | ----------------------- |
| ESM | dist/index.js |
| CJS | dist/index.cjs |
| IIFE | dist/index.global.js |
| Types | dist/index.d.ts |
License
MIT
