Items & Bento Spans
An item is any DOM element you already control — the engine only measures it and never touches its content, only its position/width-or-height/transform for placement.
Describing an item
MasonryItemDescriptor
id
string
Unique item id.
el
HTMLElement | (() => HTMLElement | null) · optional
The item's own element, or a resolver for one (e.g. an unmounted Vue/React ref that hasn't resolved yet). An item whose el is omitted, or whose resolver currently returns null, is skipped until it resolves — it doesn't error, and doesn't hold up packing the rest.
colSpan
number · default: 1
How many adjacent lanes this item spans at direction: 'vertical', clamped to [1, lanes] on every relayout — a colSpan wider than the current lane count (e.g. on a narrow viewport) is silently capped instead of overflowing.
rowSpan
number · default: 1
Same as colSpan, used at direction: 'horizontal' instead.
aspectRatio
number · optional
A provisional main-axis size (before the element has a real measured size — first mount, or not yet rendered), computed from the item's cross size divided (vertical) or multiplied (horizontal) by this ratio. Without it, such an item is left out of the layout entirely until there's a way to measure it — unless virtualize is on, in which case estimatedSize/MasonryOptions.estimateSize step in instead. One common "no measurement" case is an element that is (or contains) an unloaded <img>: the engine doesn't trust its current size and attaches its own load/error listener to the image rather than relying on ResizeObserver alone — so an image gallery with no aspectRatio on any item still lays out correctly, just with cards appearing one at a time as each image finishes loading. See Virtualization for the full estimate chain.
estimatedSize
number · optional
Explicit main-axis size estimate in px, used only while virtualize is on and this item has no real measurement yet — takes priority over aspectRatio. Ignored when virtualize is off.
order
number · optional
Explicit pack order; default is the item's position in the array passed to setItems.
Example — every field at once:
engine.setItems([
{
id: 'hero', // unique id
el: heroEl, // this item's own element
colSpan: 2, // spans 2 adjacent lanes at 'vertical' (ignored at 'horizontal', where rowSpan applies instead)
aspectRatio: 16 / 9, // provisional height (crossSize / aspectRatio) before the element is actually measured
order: 0, // packed first, regardless of array position
},
])Bento spans
A colSpan/rowSpan wider than 1 takes part in the exact same skyline packing as a regular item — the engine searches every valid starting lane for the span and picks whichever minimizes the tallest edge among the lanes it would cover, then raises all of those lanes to the new edge together:
engine.setItems([
{ id: 'hero', el: heroEl, colSpan: 2 }, // twice as wide as a regular item
{ id: 'a', el: aEl },
{ id: 'b', el: bEl },
])There's no separate CSS Grid template to keep in sync with the data — a wider item is just another entry in the same setItems() call, and the packing decides where it lands based on current lane fill, the same way a colSpan: 1 item would.