Layout Grid
A layout effect that animates the grid item on click, powered by framer motion layout
Installation
Install dependencies
npm i motion clsx tailwind-merge
Add util file
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Copy the source code
components/ui/layout-grid.tsx
"use client";
import React, { useState, useRef, useEffect } from "react";
import { AnimatePresence, motion } from "motion/react";
import { cn } from "@/lib/utils";
import Image from "next/image";
type Card = {
id: number;
content: JSX.Element | React.ReactNode | string;
className: string;
thumbnail: string;
};
export const LayoutGrid = ({ cards }: { cards: Card[] }) => {
const [selected, setSelected] = useState<Card | null>(null);
const [lastSelected, setLastSelected] = useState<Card | null>(null);
const handleClick = (card: Card) => {
setLastSelected(selected);
setSelected(card);
};
const handleOutsideClick = () => {
setLastSelected(selected);
setSelected(null);
};
return (
<div className="w-full h-full p-10 grid grid-cols-1 md:grid-cols-3 max-w-7xl mx-auto gap-4 relative">
{cards.map((card, i) => (
<div key={i} className={cn(card.className, "")}>
<motion.div
onClick={() => handleClick(card)}
className={cn(
card.className,
"relative overflow-hidden",
selected?.id === card.id
? "rounded-lg cursor-pointer absolute inset-0 h-1/2 w-full md:w-1/2 m-auto z-50 flex justify-center items-center flex-wrap flex-col"
: lastSelected?.id === card.id
? "z-40 bg-white rounded-xl h-full w-full"
: "bg-white rounded-xl h-full w-full"
)}
layoutId={`card-${card.id}`}
>
{selected?.id === card.id && <SelectedCard selected={selected} />}
<ImageComponent card={card} />
</motion.div>
</div>
))}
<motion.div
onClick={handleOutsideClick}
className={cn(
"absolute h-full w-full left-0 top-0 bg-black opacity-0 z-10",
selected?.id ? "pointer-events-auto" : "pointer-events-none"
)}
animate={{ opacity: selected?.id ? 0.3 : 0 }}
/>
</div>
);
};
const ImageComponent = ({ card }: { card: Card }) => {
return (
<motion.img
layoutId={`image-${card.id}-image`}
src={card.thumbnail}
height="500"
width="500"
className={cn(
"object-cover object-top absolute inset-0 h-full w-full transition duration-200"
)}
alt="thumbnail"
/>
);
};
const SelectedCard = ({ selected }: { selected: Card | null }) => {
return (
<div className="bg-transparent h-full w-full flex flex-col justify-end rounded-lg shadow-2xl relative z-[60]">
<motion.div
initial={{
opacity: 0,
}}
animate={{
opacity: 0.6,
}}
className="absolute inset-0 h-full w-full bg-black opacity-60 z-10"
/>
<motion.div
layoutId={`content-${selected?.id}`}
initial={{
opacity: 0,
y: 100,
}}
animate={{
opacity: 1,
y: 0,
}}
exit={{
opacity: 0,
y: 100,
}}
transition={{
duration: 0.3,
ease: "easeInOut",
}}
className="relative px-8 pb-4 z-[70]"
>
{selected?.content}
</motion.div>
</div>
);
};
Props
Prop | Type | Description |
---|---|---|
cards | Card[] | An array of Card objects. Each Card object should have the following properties: id (a unique identifier), content (the JSX.Element to be displayed), className (the CSS class name for the card), and thumbnail (the URL of the thumbnail image). |
The Card type is defined as follows:
Property | Type | Description |
---|---|---|
id | number | A unique identifier for the card. |
content | JSX.Element | React.ReactNode | string | The content to be displayed in the card. |
className | string | The CSS class name for the card. |
thumbnail | string | The URL of the thumbnail image for the card. |
Build websites faster and 10x better than your competitors with Aceternity UI Pro
With the best in class components and templates, stand out from the crowd and get more attention to your website. Trusted by founders and entrepreneurs from all over the world.
Manu was such a pleasure to work with. Talented, communicative and fast. Highly recommend!
Jonathan Barshop
Founder at Barshop Studios