Building High-Performance Masonry Layouts and Lightbox Image Previews in React
Technical guide to building responsive masonry grids, implementing progressive image loading skeletons, and creating smooth lightbox image viewers in React.
By Uttam Thapa · · Performance
⚡ Executive Summary (TL;DR)
Hundreds of mixed-aspect images, no layout jumps, no scroll lag. The recipe is smaller than it looks:
CSS columns for the masonry itself — no JavaScript geometry, no measuring pass — an aspect-ratio placeholder around every
image so the space is reserved before the bytes arrive, and a lightbox that manages focus properly instead of just appearing on top of the page.
Figure 1: Mixed aspect ratios packed without gaps — and without a single layout measurement in JavaScript.
Introduction
Rendering hundreds of high-resolution images without layout jumps, memory growth or scroll lag is one of the oldest frontend performance problems, and it has
quietly become easier: most of what used to require a JavaScript layout library is now a CSS property.
These are the techniques behind my React Image Gallery and HTML E-Commerce Storefront templates — responsive masonry, lightbox
previews, tag filtering and an image pipeline that renders instantly on a phone.
Masonry Without JavaScript
Flexbox forces every item in a row to share a height, which produces ragged whitespace as soon as portrait and landscape images sit side by side. The
Pinterest-style packing everyone actually wants comes free from CSS columns.
/* Responsive CSS masonry grid */
.masonry-grid {
column-count: 1;
column-gap: 1.5rem;
}
@media (min-width: 640px) { .masonry-grid { column-count: 2; } }
@media (min-width: 1024px) { .masonry-grid { column-count: 3; } }
.masonry-item {
break-inside: avoid;
margin-bottom: 1.5rem;
}
| Approach |
Cost |
Trade-off |
| CSS columns |
Zero JavaScript |
Order flows top-to-bottom per column, not left-to-right |
| JS masonry library |
Measure and position every item |
Exact reading order, at the price of a layout pass on every resize |
| CSS grid + row spans |
Minimal, needs known ratios |
Correct order, but each item's span must be computed from its aspect ratio |
Columns fill vertically, so item two sits below item one rather than beside it. For a gallery that is fine — nobody reads photographs in sequence. For anything
where order carries meaning, use the grid approach instead.
Zero Layout Shift, by Reserving Space
A browser does not know an image's dimensions until enough of the file has arrived to read its header. Until then the element has no height, so everything below
it jumps the moment it loads — the single largest source of CLS in any image-heavy page.
// The wrapper holds the space; the image fills it when it arrives.
<div style={{ aspectRatio: '16 / 9' }} className="bg-slate-800 animate-pulse">
<img
src={imageSrc}
alt={caption}
className="w-full h-full object-cover"
/>
</div>
💡 Store the aspect ratio with the image
This only works if you know the ratio before loading, so record width and height at upload time and serve them with the image metadata. Guessing a single
ratio for a mixed gallery replaces layout shift with letterboxing — a quieter problem, but still the wrong picture.
Defers off-screen images. Never apply it to anything above the fold — that is your LCP element.
Keeps image decode off the critical path so scrolling stays smooth while images arrive.
Responsive sizes
A srcset with real sizes stops phones downloading desktop-width originals.
A Lightbox That Behaves
Clicking a thumbnail opens a full-resolution viewer with scale and opacity transitions, keyboard shortcuts — Escape to close,
ArrowLeft and ArrowRight to move — and drag-to-dismiss on touch.
🚨 The accessibility details that get skipped
- Trap focus inside the dialog while it is open, and restore it to the thumbnail on close.
- Lock body scroll so the page behind does not drift under the overlay.
- Label it with
role="dialog", aria-modal="true" and an accessible name.
- Preload the neighbours — the next and previous images — so arrow navigation is instant.
A lightbox that a keyboard user cannot escape is worse than no lightbox at all, and it is the most common failure in gallery components. Everything on that list
is a few lines each.
✅ Key takeaways
- ✓Use
column-count for masonry. Native packing with no JavaScript geometry and no resize cost.
- ✓Reserve space with
aspect-ratio. This is the whole of CLS in an image gallery.
- ✓Store dimensions at upload time. You cannot reserve space for a ratio you do not know.
- ✓Pair
loading="lazy" with decoding="async" — but never on the above-the-fold image.
- ✓Manage focus in the lightbox. Trap it, restore it, and close on Escape.
More on the metric behind all of this in improving Core Web Vitals in React applications and
eliminating layout jank at 60fps.
Frequently asked questions
How do you prevent layout shift in a masonry image grid?
Reserve each cell's space before the image loads using its known aspect ratio. A grid that sizes itself from loaded images will reflow on every arrival.
When is virtualisation worth adding to a gallery?
Once the item count is high enough that DOM node count, not image bandwidth, is the bottleneck — typically several hundred items. Below that it adds complexity without a measurable gain.
How do you keep a lightbox accessible?
Trap focus inside it while open, restore focus to the trigger on close, close on Escape, and label the dialog. A lightbox that traps a keyboard user is worse than no lightbox.
Home · Projects · Blog · Services · Résumé · Contact