npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bndynet/layout

v0.1.0

Published

Lightweight, headless, framework-agnostic layout library with grid, flex and absolute engines.

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 any in 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/layout

Via 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. 1000md). 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 lock field is reserved for this).
  • Resize (minW/minH/maxW/maxH are already part of the schema).
  • Collision with push compaction (the grid collision / compact fields 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