Cards
Masonry Grid
#grid#masonry#gallery
implementedInfinityUI component
This component is fully implemented in InfinityUI and wired into the docs and registry flow.
Installation
Use the registry command to add the component source, and install any package dependencies if needed.
Infinity Registry
Install the component source into your project with the shadcn CLI.
npx shadcn@latest add https://infinityui-pearl.vercel.app/r/masonry-gridPackage Dependencies
Install the npm packages used by this component source.
npm install framer-motionComponent Code
Copy and paste this code into your component file.
tsx
"use client";
import { useState } from "react";
import Image from "next/image";
import { motion } from "framer-motion";
export interface MasonryGridProps {
items: { image: string; title: string; description: string }[];
columns?: number;
}
export function MasonryGrid({ items, columns }: MasonryGridProps) {
const [imagesLoaded, setImagesLoaded] = useState<Record<string, boolean>>({});
if (!items?.length) {
return <div className="p-4 text-center text-sm text-zinc-400">No items to display</div>;
}
const columnCount = columns ?? 4;
return (
<div
style={{ columns }}
className={`${!columns ? "columns-1 sm:columns-2 md:columns-3 lg:columns-4" : ""} w-full max-w-4xl gap-2 overflow-y-auto p-3`}
>
{items.map((item, index) => {
const rowIndex = Math.floor(index / columnCount);
return (
<motion.div
key={`${item.title}-${index}`}
className="group relative mb-4 break-inside-avoid overflow-hidden rounded-xl border border-transparent p-1 hover:border-neutral-300 dark:hover:border-neutral-700"
initial={{ opacity: 0, y: 20 }}
animate={{
opacity: 1,
y: 0,
transition: {
duration: 0.5,
delay: rowIndex * 0.1,
ease: "easeOut",
},
}}
whileHover={{ scale: 1.05 }}
>
<div className="relative flex w-full flex-col items-start justify-start gap-1">
{!imagesLoaded[item.image] && (
<div className="absolute inset-0 h-[300px] w-full animate-pulse rounded-lg bg-neutral-500/50" />
)}
<Image
src={item.image}
alt={item.title}
width={400}
height={300}
sizes="(max-width: 640px) 100vw, (max-width: 768px) 50vw, (max-width: 1024px) 33vw, 25vw"
className={`h-auto w-full rounded-lg transition-transform duration-300 ${
!imagesLoaded[item.image] ? "opacity-0" : "opacity-100"
}`}
onLoad={() =>
setImagesLoaded((previous) => ({
...previous,
[item.image]: true,
}))
}
onError={() =>
setImagesLoaded((previous) => ({
...previous,
[item.image]: true,
}))
}
/>
{imagesLoaded[item.image] && (
<div className="w-full">
<h3 className="text-sm font-medium">{item.title}</h3>
<p className="mt-0 line-clamp-2 overflow-hidden text-xs text-neutral-500">
{item.description}
</p>
</div>
)}
</div>
</motion.div>
);
})}
</div>
);
}
export default MasonryGrid;