A high-performance, virtualized masonry grid component for React with dynamic column layout, lazy loading, and interactive 3D zoom effects.
- 🚀 High Performance: Virtual scrolling renders only visible items
- 📱 Responsive: Automatically adjusts columns based on container width
- 🎨 Flexible: Works with any content type (images, cards, etc.)
- 💪 TypeScript: Full type safety and IntelliSense support
- ⚡ Optimized: Uses RAF, memoization, and CSS containment
- 🎯 Zero Dependencies: Only peer dependencies on React
- 📦 Lightweight: < 7KB minified
- ♾️ Infinite Scroll: Built-in
onEndReachedcallback - 🖥️ SSR Ready: Placeholder support for hydration
- 🦴 Skeleton Loading: Pixel-perfect skeleton cards auto-sized to actual column widths
- 🔍 Zoom-on-Hover: Hold
Z+ hover for 3D perspective tilt with dynamic shadows - 🎯 Programmatic Scroll: Scroll to any item by index using
scrollToIndexviaref - 🪟 Custom Scroller: Drop-in support for OverlayScrollbars, SimpleBar, Lenis, and any custom scroll container
npm install react-masonry-virtualizedyarn add react-masonry-virtualizedpnpm add react-masonry-virtualizedimport { MasonryGrid, getImageSize } from 'react-masonry-virtualized';
const images = [
'https://example.com/image1.jpg',
'https://example.com/image2.jpg',
// ... more images
];
function App() {
return (
<MasonryGrid
items={images}
renderItem={(src, index) => (
<img
src={src}
alt={`Image ${index}`}
loading="lazy"
style={{ width: '100%', height: 'auto', display: 'block' }}
/>
)}
getItemSize={async (src) => await getImageSize(src)}
gap={16}
minWidth={280}
/>
);
}import { MasonryGrid, getImageSize } from 'react-masonry-virtualized';
function App() {
const [images, setImages] = useState(initialImages);
const [loading, setLoading] = useState(false);
const loadMore = async () => {
if (loading) return;
setLoading(true);
const newImages = await fetchMoreImages();
setImages(prev => [...prev, ...newImages]);
setLoading(false);
};
return (
<MasonryGrid
items={images}
renderItem={(src, index) => (
<img src={src} alt={`Image ${index}`} loading="lazy" />
)}
getItemSize={async (src) => await getImageSize(src)}
onEndReached={loadMore}
onEndReachedThreshold={500}
/>
);
}If you already know item dimensions, return them immediately for better performance:
<MasonryGrid
items={posts}
renderItem={(post) => <PostCard post={post} />}
getItemSize={(post) => Promise.resolve({
width: post.width,
height: post.height
})}
/><MasonryGrid
items={images}
renderItem={(src) => <img src={src} />}
getItemSize={async (src) => await getImageSize(src)}
ssrPlaceholder={
<div className="grid grid-cols-3 gap-4">
{[...Array(9)].map((_, i) => (
<div key={i} className="h-64 bg-gray-200 animate-pulse rounded" />
))}
</div>
}
/>Pass a single card template via loadingPlaceholder and the library renders it
in the exact same columns as the real grid — you get perfectly-aligned
skeletons without duplicating any layout logic yourself.
function SkeletonCard() {
return (
<div
style={{
width: '100%',
height: '100%',
borderRadius: 16,
background: 'linear-gradient(90deg, #e2e8f0 25%, #f1f5f9 50%, #e2e8f0 75%)',
backgroundSize: '200% 100%',
animation: 'shimmer 1.4s infinite',
}}
/>
);
}
function App() {
const [pins, setPins] = useState([]);
const [isLoading, setIsLoading] = useState(true);
return (
<MasonryGrid
items={pins}
renderItem={(pin) => <PinCard pin={pin} />}
getItemSize={(pin) => Promise.resolve({ width: pin.w, height: pin.h })}
// Library handles layout — skeletons match the real columns & widths
loadingPlaceholder={<SkeletonCard />}
skeletonCount={12} // how many cards to show (default: 12)
skeletonAspectRatio={1.3} // card height / width ratio (default: 1.3)
/>
);
}Note:
loadingPlaceholderis active whilegetItemSizeis resolving. Once dimensions are loaded the real grid replaces it. For SSR hydration usessrPlaceholderas before.
Hold the Z key and hover over any card to zoom it with a 3D perspective tilt and dynamic shadow. Cards tilt toward your cursor with realistic depth.
<MasonryGrid
items={images}
renderItem={(src) => <img src={src} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />}
getItemSize={async (src) => await getImageSize(src)}
enableZoomOnHover // Enable the feature
zoomScale={1.1} // 10% larger on zoom (default: 1.08)
/>How it works:
- Press and hold
Z→ hover a card → card scales up with smooth animation - Move mouse → card tilts in 3D (±15°) with shadow shifting opposite to the tilt
- Release
Zor leave the card → instantly snaps back (no animation) - Must release and re-press
Zfor each zoom cycle — prevents accidental continuous zooming
You can scroll to a specific item by index using the scrollToIndex method via a React ref.
import { useRef } from 'react';
import { MasonryGrid, MasonryGridRef } from 'react-masonry-virtualized';
function App() {
const gridRef = useRef<MasonryGridRef>(null);
const handleScroll = () => {
// Scroll to item at index 50
gridRef.current?.scrollToIndex(50, {
behavior: 'smooth',
offset: 20
});
};
return (
<>
<button onClick={handleScroll}>Scroll to 50</button>
<MasonryGrid ref={gridRef} items={items} ... />
</>
);
}By default the grid listens for scroll events on window. Wrap the grid inside
any custom scroller and pass its scrollable viewport element via scrollContainer
to keep virtualization, infinite scroll, and scrollToIndex working correctly.
import { useRef } from 'react';
import { useOverlayScrollbars } from 'overlayscrollbars-react';
import 'overlayscrollbars/overlayscrollbars.css';
import { MasonryGrid } from 'react-masonry-virtualized';
function App() {
const containerRef = useRef<HTMLDivElement>(null);
const [initialize, getInstance] = useOverlayScrollbars({
options: { scrollbars: { autoHide: 'scroll' } },
});
// initialize OverlayScrollbars on the wrapper div
useEffect(() => {
if (containerRef.current) initialize(containerRef.current);
}, [initialize]);
// get the inner viewport element that actually scrolls
const viewport = getInstance()?.elements().viewport;
return (
<div ref={containerRef} style={{ height: '100vh' }}>
<MasonryGrid
items={items}
renderItem={(item) => <Card item={item} />}
getItemSize={(item) => Promise.resolve({ width: item.w, height: item.h })}
scrollContainer={viewport}
/>
</div>
);
}import { useRef } from 'react';
import { MasonryGrid } from 'react-masonry-virtualized';
function App() {
const scrollRef = useRef<HTMLDivElement>(null);
return (
<div
ref={scrollRef}
style={{ height: '100vh', overflowY: 'auto' }}
>
<MasonryGrid
items={items}
renderItem={(item) => <Card item={item} />}
getItemSize={(item) => Promise.resolve({ width: item.w, height: item.h })}
scrollContainer={scrollRef} // pass the ref directly
/>
</div>
);
}Note: The scroll container element must have
overflow: autooroverflow-y: scrolland a fixed height. TheMasonryGriditself should not have a fixed height when using a custom container — let it grow naturally inside the scrollable parent.
<MasonryGrid
items={images}
columnCount={4} // Always 4 columns
// ... other props
/>| Prop | Type | Default | Description |
|---|---|---|---|
items |
T[] |
required | Array of items to render |
renderItem |
(item: T, index: number) => ReactNode |
required | Function to render each item |
getItemSize |
(item: T, index: number) => Promise<{width, height}> |
required | Function to get item dimensions |
baseWidth |
number |
241 |
Base width for scaling calculations |
minWidth |
number |
223 |
Minimum width for each column |
gap |
number |
16 |
Gap between items in pixels |
className |
string |
'' |
Container class name |
style |
CSSProperties |
undefined |
Container inline styles |
bufferMultiplier |
number |
1 |
Viewport buffer (1 = 1 viewport above/below) |
columnCount |
number |
undefined |
Override auto column count |
onEndReached |
() => void |
undefined |
Callback when scrolled near end |
onEndReachedThreshold |
number |
500 |
Distance from end to trigger callback (px) |
ssrPlaceholder |
ReactNode |
undefined |
Placeholder during SSR/hydration (before JS runs) |
disableVirtualization |
boolean |
false |
Render all items (disables virtual scroll) |
loadingPlaceholder |
ReactNode |
undefined |
Single card template tiled in masonry columns while loading |
skeletonCount |
number |
12 |
Number of skeleton cards to render |
skeletonAspectRatio |
number |
1.3 |
Height/width ratio used for skeleton card sizing |
enableZoomOnHover |
boolean |
false |
Hold Z key + hover to zoom & 3D-tilt cards |
zoomScale |
number |
1.08 |
Scale multiplier when zoom is active (e.g. 1.1 = 10% larger) |
scrollContainer |
HTMLElement | RefObject<HTMLElement> | null |
undefined |
Custom scroll container — pass an element or ref when using OverlayScrollbars, SimpleBar, Lenis, etc. |
ref |
Ref<MasonryGridRef> |
undefined |
Ref to access imperative methods |
| Method | Arguments | Description |
|---|---|---|
scrollToIndex |
(index: number, options?: { behavior: 'smooth' | 'auto', offset: number }) |
Scrolls the grid to the item at the specified index. |
Helper function to load image dimensions. Useful for image-based masonry grids.
import { getImageSize } from 'react-masonry-virtualized';
const dimensions = await getImageSize('https://example.com/image.jpg');
// { width: 1920, height: 1080 }- Dynamic Columns: Calculates optimal number of columns based on container width and
minWidth - Masonry Layout: Places items in the shortest column (Pinterest-style)
- Virtual Scrolling: Only renders items visible in viewport + buffer
- Performance Optimization:
React.memoprevents unnecessary re-rendersuseCallbackmemoizes expensive calculationsrequestAnimationFramethrottles scroll events- Debounced resize handler
- CSS containment for layout isolation
- GPU-accelerated transforms with
translate3d
- Memoize
getItemSize: If dimensions don't change, cache them - Use pre-computed dimensions: Return
Promise.resolve()for known sizes - Adjust
bufferMultiplier: Lower values render fewer items (faster) but may show blank space while scrolling - Use
loading="lazy": For images, enable native lazy loading - Optimize images: Use appropriately sized images
Benchmarked with 500 items, scrolling 5000px down and back up. Measured on Chrome.
| Library | Avg FPS | Min FPS | Memory |
|---|---|---|---|
| 🏆 react-masonry-virtualized | 60 | 59 | 54 MB |
| masonic | 60 | 57 | 48 MB |
| react-virtualized | 60 | 54 | 70 MB |
| @tanstack/react-virtual | 59 | 56 | 49 MB |
Bundle size: 6.5 KB minified · 2 KB gzipped · Zero dependencies
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
MIT
Contributions welcome! Please open an issue or PR.
