masonry-blade is a tiny, fast, and extensible engine for masonry grid calculation with zero dependencies 🤤
- 🪶 Lightweight - It solves exactly one problem, and solves it well.
- ⚡ Fast - Greedy balancing, k-way merge, and very little overhead.
- ⚖️ Balanced - Each next item goes into the shortest column.
- 📦 Zero dependencies - Nothing extra.
- 🎨 UI-agnostic - Works with
React,Vue,Svelte,Canvas, andVanilla JS. - 🏷️ Supports metadata - Grid items can carry any extra data you need for rendering.
- 💤 Supports lazy-load - Append items in batches without losing the current layout.
- 🔄 Can rebuild the matrix - Recalculate the layout for a new width, column count, or
gap. - 🧵 Can run through
Web Worker- And if it is unavailable or disabled, it falls back to sync calculation.
You pass in source item sizes, and it returns ready-to-render
x,y,width, andheightfor any UI. No DOM, no framework coupling, and no bloated API.
npm i masonry-bladeyarn add masonry-bladepnpm add masonry-bladeMasonryMatrix- the main facadeMasonryMatrixErrorandMASONRY_MATRIX_ERROR_MESSAGES- facade errors and constants- Masonry TS types:
MasonryMatrixErrorMessage,MasonryMatrixState, andRecreateOptions - Engine TS types:
MatrixSourceUnit,MatrixComputedUnit,ReadonlySortItems,ReadonlyMatrixandMatrixSnapshot
import {
MasonryMatrix,
MasonryMatrixError,
MASONRY_MATRIX_ERROR_MESSAGES,
type MatrixSourceUnit,
type MatrixComputedUnit,
type ReadonlySortItems,
type ReadonlyMatrix,
type MatrixSnapshot,
type MasonryMatrixErrorMessage,
type MasonryMatrixState,
type RecreateOptions,
} from 'masonry-blade';new MasonryMatrix<Meta = undefined>(rootWidth: number, columnCount: number, gap: number)rootWidth- container widthcolumnCount- number of columnsgap- horizontal space between columns and vertical space between items
All three constructor arguments are required.
await matrix.append(items);Appends a new batch of items to the current matrix and returns the columns.
await matrix.sort(source);Transforms source into one flat array sorted by visual order: first by y, then by x.
It does not rebuild the matrix and does not mutate the current facade state.
await matrix.recreate({
rootWidth,
columnCount?,
gap?,
items?,
})Rebuilds the matrix from scratch using only the items passed in options.
If items are omitted, the matrix is rebuilt as an empty layout.
If columnCount and gap are omitted, the last successful values are reused. At first these are the constructor values, then the values from the last successful recreate(...).
matrix.terminateWorker();Stops the current Worker, if one was created. If a worker calculation is still running, the current Promise is rejected.
matrix.disableWorker();Stops the current Worker and forces all following calculations into sync mode.
matrix.enableWorker();Re-enables worker mode and immediately tries to create a new Worker. If the environment does not support it or creation fails, the library stays in sync mode.
const state = matrix.getState();Returns a snapshot of the current facade state: columnCount, columnWidth, gap, workerCreated, workerDisabled, and copies of columnsHeights and order.
This is a safe way to inspect service state without touching live internal columns.
Input items are plain objects with this shape:
{
id: string | number;
width: number;
height: number;
meta?: Meta;
}
// MatrixComputedUnit<Meta>append() and recreate() return a matrix with this shape:
readonly (readonly Readonly<{
id: string | number;
width: number;
height: number;
x: number;
y: number;
meta?: Meta;
}>[])[]
// ReadonlyMatrix<Meta>sort() returns a flat list with this shape:
readonly Readonly<{
id: string | number;
width: number;
height: number;
x: number;
y: number;
meta?: Meta;
}>[]
// ReadonlySortItems<Meta>If you create new MasonryMatrix<Meta>(...), any provided meta value is typed as Meta, and output items keep the same meta.
Important: output width and height are already scaled to the column width. They are not the original dimensions.
import { MasonryMatrix } from 'masonry-blade';
const matrix = new MasonryMatrix(1200, 3, 16);
const columns = await matrix.append([
{ id: '1', width: 1600, height: 900 },
{ id: '2', width: 800, height: 1200 },
{ id: '3', width: 1000, height: 1000 },
]);
const items = await matrix.sort(columns);
console.log(items);append(), sort(), and recreate() are always async. Even if Worker is not used, you still work through await.
Internally, everything is simple:
- First, the column width is calculated:
columnWidth = (rootWidth - gap * (columnCount - 1)) / columnCount;- Item height is scaled by the original aspect ratio.
- The next item is placed into the shortest column.
- The library calculates
xandyfor a virtual canvas.
This gives you a fast and visually even layout without complex heuristics.
meta does not participate in the calculation, but it travels through the whole matrix together with the item.
If you create MasonryMatrix<Meta>, any provided meta value is typed as Meta.
import { MasonryMatrix } from 'masonry-blade';
type PhotoMeta = {
src: string;
alt: string;
author: string;
};
const matrix = new MasonryMatrix<PhotoMeta>(960, 2, 12);
const columns = await matrix.append([
{
id: 'photo-1',
width: 1600,
height: 900,
meta: {
src: '/images/1.jpg',
alt: 'Mountain lake',
author: 'Kate',
},
},
]);
console.log(columns[0][0].meta.src);append() and recreate() return columns. If you need one flat list for rendering from top to bottom and then from left to right, call sort(...).
import { MasonryMatrix } from 'masonry-blade';
const matrix = new MasonryMatrix(1200, 3, 16);
const columns = await matrix.append([
{ id: '1', width: 1600, height: 900 },
{ id: '2', width: 800, height: 1200 },
{ id: '3', width: 1000, height: 1000 },
]);
const orderedItems = await matrix.sort(columns);
console.log(orderedItems.map((item) => item.id));Below is a self-contained Vanilla JS example with absolute positioning:
import { MasonryMatrix } from 'masonry-blade';
const container = document.createElement('div');
container.style.position = 'relative';
window.document.body.append(container);
const initialItems = [
{
id: 'photo-1',
width: 1600,
height: 900,
meta: {
title: 'Mountain lake',
},
},
{
id: 'photo-2',
width: 900,
height: 1350,
meta: {
title: 'Pine forest',
},
},
{
id: 'photo-3',
width: 1200,
height: 800,
meta: {
title: 'Green Apple',
},
},
{
id: 'photo-4',
width: 900,
height: 900,
meta: {
title: 'Red Apple',
},
},
];
const render = (placedItems) => {
container.innerHTML = '';
for (const item of placedItems) {
const node = document.createElement('div');
node.style.position = 'absolute';
node.style.left = `0px`;
node.style.top = `0px`;
node.style.transform = `translate(${item.x}px, ${item.y}px)`;
node.style.width = `${item.width}px`;
node.style.height = `${item.height}px`;
node.style.background = 'red';
container.appendChild(node);
}
};
async function main() {
const matrix = new MasonryMatrix(container.clientWidth, 4, 16);
const columns = await matrix.append(initialItems);
const placedItems = await matrix.sort(columns);
render(placedItems);
}
main();recreate() recalculates the grid from the explicit list of items you pass in.
import { MasonryMatrix } from 'masonry-blade';
const container = document.createElement('div');
container.style.position = 'relative';
window.document.body.append(container);
const initialItems = [
{
id: 'photo-1',
width: 1600,
height: 900,
meta: {
title: 'Mountain lake',
},
},
{
id: 'photo-2',
width: 900,
height: 1350,
meta: {
title: 'Pine forest',
},
},
{
id: 'photo-3',
width: 1200,
height: 800,
meta: {
title: 'Green Apple',
},
},
{
id: 'photo-4',
width: 900,
height: 900,
meta: {
title: 'Red Apple',
},
},
];
const render = (placedItems) => {
container.innerHTML = '';
for (const item of placedItems) {
const node = document.createElement('div');
node.style.position = 'absolute';
node.style.left = `0px`;
node.style.top = `0px`;
node.style.transform = `translate(${item.x}px, ${item.y}px)`;
node.style.width = `${item.width}px`;
node.style.height = `${item.height}px`;
node.style.background = 'red';
container.appendChild(node);
}
};
async function main() {
const matrix = new MasonryMatrix(container.clientWidth, 4, 16);
const initialColumns = await matrix.append(initialItems);
const initialPlacedItems = await matrix.sort(initialColumns);
render(initialPlacedItems);
window.addEventListener('resize', async () => {
const width = container.clientWidth;
const columns = width < 768 ? 2 : width < 1200 ? 3 : 4;
const rebuilt = await matrix.recreate({
rootWidth: width,
columnCount: columns,
items: initialItems,
});
const rebuiltItems = await matrix.sort(rebuilt);
render(rebuiltItems);
});
}
main();If you call only recreate({ rootWidth: newWidth }), the library uses the last successful columnCount and gap, but because items are not passed, the rebuilt layout will be empty.
import { MasonryMatrix } from 'masonry-blade';
const matrix = new MasonryMatrix(1200, 3, 16);
let columns;
matrix.disableWorker();
columns = await matrix.append([{ id: 1, width: 1, height: 1 }]); // guaranteed sync calculation
matrix.enableWorker();
columns = await matrix.recreate({
rootWidth: 1200,
items: [{ id: 1, width: 1, height: 1 }],
}); // the library tries to use Worker again
console.log(columns);- Treat the returned layout as read-only. Container arrays are safe to read, but mutating item objects themselves is not part of the public contract.
- Treat input items as immutable while a call is running.
- In worker mode, the payload goes through structured clone, so values inside
metamust also be cloneable. sort()only reads thesourceyou pass in and does not modify the stored matrix state.
append()andrecreate({ items })silently skip items whoseidis neither a finite number nor a non-empty string, or whosewidthorheightis not a positive finite number.
terminateWorker(),disableWorker(), andenableWorker()can interrupt a running worker calculation.- If
Workeris unavailable or creation fails, the library automatically falls back to the synchronous path and stays there until you explicitly callenableWorker(). - After
disableWorker(), sync mode stays active until you explicitly callenableWorker(). - If calculation runs through a
Worker, data insidemetamust support structured clone. Functions, DOM nodes, and similar values may fail onpostMessage(...).
getState()returns a snapshot of service state. ItscolumnsHeightsandorderfields are cloned, so you can read them without risking damage to the internal state.
- The library has no API for removing, updating, or selectively reordering individual items.
- The library does not work with the DOM by itself. Measuring the container, choosing breakpoints, and rendering are your responsibility.
recreate()does not automatically reuse previous items fromappend(). If you need a rebuild from source data, passitemsexplicitly.
The library validates matrix parameters and filters items.
Invalid matrix parameters:
rootWidth <= 0orrootWidthis not a finite numbercolumnCount <= 0orcolumnCountis not an integergap < 0orgapis not a finite numbergapleaves no positive space for columns
Invalid items in append() or recreate({ items }) do not throw. They are skipped instead. An item is skipped if its id is neither a finite number nor a non-empty string, or if its width or height is not a positive finite number.
Top-level errors come as MasonryMatrixError. The original reason may appear inside cause, including a Worker error, a structured clone / postMessage(...) error, or an engine validation error.
Typical top-level messages:
Failed to append items to the matrixFailed to sort source matrixFailed to recreate the matrix
Typical internal causes in cause:
Failed to receive a message from the workerWorker execution failedWorker was terminated
pnpm test:benchCurrent benchmark suites live in:
src/core/LayoutCalculationEngine/__test__/runtime/Matrix/Matrix.bench.tssrc/utils/__tests__/kWayMerge.bench.ts
See CONTRIBUTING.md
See SECURITY.md
The project is distributed under the MPL 2.0 license.
- Author: @steelWinds
- Issues: Open an issue
- Telegram: @plutograde
- Email: Send an email