A Pinterest-style image feed that places images left to right into the shortest column using manual height calculation — no CSS grid required. Includes an optional fullscreen lightbox with carousel navigation.
Images are placed left to right into the shortest column — no CSS grid, no column-count, pure arithmetic.
Aspect ratios are always preserved — column width is fixed and height is derived from each image's natural ratio.
Unknown image dimensions are measured via invisible probe elements; if you supply width and height on each image object, layout runs instantly with no flicker.
Container width is tracked via ResizeObserver — layout recalculates automatically on resize.
Configurable columns, gap, and border radius via props.
Optional click handler and hover overlay slot for custom UI on each card.
Optional fullscreen lightbox — opens on card click with a smooth backdrop blur overlay.
Optional carousel mode inside the lightbox — prev/next buttons, keyboard arrow navigation, a position counter, and dot indicators (shown when the image count is ≤ 20).
pointer-events: none is never set on the container — cards are fully interactive.
The layout engine tracks a colHeights[] array — one entry per column. For each image it finds the column with the minimum current height, places the image there, and increments that column's height by the card's calculated height plus the gap.
When width and height are omitted on an image object, an invisible 1×1 <img> probe is mounted to trigger a natural-size onLoad. Once measured, the ratio is stored and layout re-runs. This only happens once per unique src.
Cards use position: absolute inside a position: relative container whose height is set to the tallest column's total height. This means the component grows naturally in the document flow — no fixed height needed on a parent.
ResizeObserver watches the container element directly, so layout recalculates correctly inside drawers, modals, sidebars, or any other collapsible container.
The lightbox locks document.body scroll while open and restores it on close. It closes on backdrop click, close button click, or the Escape key.
In carousel mode, the ← / → arrow keys navigate between images. Navigation wraps around — pressing next on the last image goes back to the first.
Dot indicators appear at the bottom of the lightbox only when carousel is true and the image array contains 20 or fewer items. Clicking a dot jumps directly to that image.
carousel has no effect unless lightbox is also true. The component enforces this internally — setting carousel={true} alone is silently ignored.
For responsive column counts, manage columns externally with a useMediaQuery hook or a CSS-breakpoint-aware value rather than hardcoding a number.