react-masonry-virtualized
v2.3.2
Published
A high-performance, virtualized masonry grid component for React with dynamic column layout, infinite scroll, and async image loading
Maintainers
Readme
react-masonry-virtualized
A high-performance, virtualized masonry grid component for React with dynamic column layout, lazy loading, and interactive 3D zoom effects.
Features
- 🚀 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
Installation
npm install react-masonry-virtualizedyarn add react-masonry-virtualizedpnpm add react-masonry-virtualizedUsage
Basic Example with Images
import { 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}
/>
);
}Infinite Scroll Example
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}
/>
);
}Pre-computed Dimensions (Faster)
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
})}
/>SSR with Loading Placeholder
<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>
}
/>Loading Skeleton (Pixel-Perfect Alignment)
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.
Zoom-on-Hover (3D Tilt)
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
Programmatic Scrolling
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} ... />
</>
);
}Custom Scroll Container (OverlayScrollbars, SimpleBar, Lenis, …)
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.
With OverlayScrollbars
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>
);
}With a plain scrollable div (or any other library)
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.
Fixed Column Count
<MasonryGrid
items={images}
columnCount={4} // Always 4 columns
// ... other props
/>API
MasonryGrid 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 |
MasonryGridRef Methods
| Method | Arguments | Description |
|--------|-----------|-------------|
| scrollToIndex | (index: number, options?: { behavior: 'smooth' \| 'auto', offset: number }) | Scrolls the grid to the item at the specified index. |
Helper Functions
getImageSize(src: string): Promise<{width, height}>
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 }How It Works
- 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
Performance Tips
- 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
Performance Benchmarks
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
Browser Support
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
License
MIT
Contributing
Contributions welcome! Please open an issue or PR.
Credits
Built with ❤️ using React, TypeScript, and tsup.
