Masonry
Overview
The Masonry component creates Pinterest-style layouts that optimize vertical space by arranging items of varying heights into responsive columns.
It supports responsive breakpoints, gaps, balanced column heights, and entrance animations.
Preview
- Preview
- Code
Item 1
Item 4 (tallest)
Item 2 (taller)
Item 3
import Masonry from './components/ui';
function MasonryDemo() {
return (
<div className="p-4 border rounded-lg">
<Masonry
columns={3}
mobile={1}
gap="normal"
balanced
animation="fade-in"
>
<div className="bg-red-600 text-white p-4 rounded">Item 1</div>
<div className="bg-red-600 text-white p-10 rounded">Item 2 (taller)</div>
<div className="bg-red-600 text-white p-6 rounded">Item 3</div>
<div className="bg-red-600 text-white p-20 rounded">Item 4 (tallest)</div>
</Masonry>
</div>
);
}
Installation
- npm
- Yarn
- pnpm
- manual
npx @mindfiredigital/ignix-ui add masonry
yarn @mindfiredigital/ignix-ui add masonry
pnpm @mindfiredigital/ignix-ui add masonry
import React, { ReactNode, useEffect, useState } from "react";
import { motion } from "framer-motion";
type Gap = "none" | "small" | "normal" | "large" | string;
type Animation = "none" | "fade-in" | "scale-in" | "slide-up";
interface MasonryProps {
children: ReactNode[];
columns?: number; // desktop
mobile?: number; // mobile
gap?: Gap;
balanced?: boolean;
animation?: Animation;
className?: string;
}
const gapMap: Record<Exclude<Gap, string>, string> = {
none: "0",
small: "0.5rem",
normal: "1rem",
large: "2rem",
};
const Masonry: React.FC<MasonryProps> = ({
children,
columns = 3,
mobile = 1,
gap = "normal",
balanced = true,
animation = "none",
className = "",
}) => {
const computedGap = gapMap[gap as keyof typeof gapMap] || gap;
const [columnItems, setColumnItems] = useState<ReactNode[][]>([]);
useEffect(() => {
if (!balanced) return;
const colHeights = new Array(columns).fill(0);
const colItems: ReactNode[][] = Array.from({ length: columns }, () => []);
React.Children.forEach(children, (child) => {
const shortest = colHeights.indexOf(Math.min(...colHeights));
colItems[shortest].push(child);
colHeights[shortest] += 1;
});
setColumnItems(colItems);
}, [children, columns, balanced]);
const variants = {
initial: { opacity: 0, y: animation === "slide-up" ? 20 : 0, scale: animation === "scale-in" ? 0.95 : 1 },
animate: { opacity: 1, y: 0, scale: 1 },
};
if (balanced) {
return (
<div
className={`flex w-full ${className}`}
style={{ gap: computedGap }}
>
{columnItems.map((col, i) => (
<div key={i} className="flex flex-col" style={{ gap: computedGap, flex: 1 }}>
{col.map((child, j) => (
animation === "none" ? (
<div key={j}>{child}</div>
) : (
<motion.div
key={j}
initial="initial"
animate="animate"
variants={variants}
transition={{ duration: 0.3, delay: j * 0.05 }}
>
{child}
</motion.div>
)
))}
</div>
))}
</div>
);
}
return (
<div
className={`masonry ${className}`}
style={{
columnCount: mobile,
columnGap: computedGap,
}}
>
<style>
{`
@media (min-width: 768px) {
.masonry {
column-count: ${columns};
column-gap: ${computedGap};
}
}
`}
</style>
{React.Children.map(children, (child, idx) =>
animation === "none" ? (
<div style={{ breakInside: "avoid", marginBottom: computedGap }}>
{child}
</div>
) : (
<motion.div
key={idx}
initial="initial"
animate="animate"
variants={variants}
transition={{ duration: 0.3, delay: idx * 0.05 }}
style={{ breakInside: "avoid", marginBottom: computedGap }}
>
{child}
</motion.div>
)
)}
</div>
);
};
export default Masonry;
Variants
- Preview
- Code
Item 1
Item 4
Item 2
Item 5
Item 3
Item 6
<Masonry
columns={3}
mobile={1}
gap="normal"
balanced={true}
animation="none"
>
{items.map((item, i) => (
<div key={i} className="bg-gray-200 rounded-lg shadow text-center">
Item {i + 1}
</div>
))}
</Masonry>
Usage
Basic Example
function Gallery() {
return (
<Masonry columns={3} mobile={1} gap="normal">
{images.map((src, i) => (
<img key={i} src={src} className="rounded shadow" />
))}
</Masonry>
);
}
Balanced Columns
<Masonry columns={4} mobile={2} gap="small" balanced>
{items.map((item, i) => (
<Card key={i} {...item} />
))}
</Masonry>
With Animation
<Masonry columns={3} animation="scale-in">
{products.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</Masonry>
Props
Prop | Type | Default | Description |
---|---|---|---|
columns | number | 3 | Number of columns at desktop. |
mobile | number | 1 | Number of columns at mobile breakpoint. |
gap | "none" | "small" | "normal" | "large" | string | "normal" | Gap between items. |
balanced | boolean | true | If true , items are distributed evenly to balance column heights. |
animation | "none" | "fade-in" | "scale-in" | "slide-up" | "none" | Defines entrance animation for items. |
className | string | "" | Custom class names for wrapper. |