Docs

Layout Grid

A layout effect that animates the grid item on click, powered by framer motion layout

Installation

Install dependencies

npm install framer-motion lucide-react

Update tailwind.config.js

Add the following to your tailwind.config.js file.

module.exports = {
  theme: {
    extend: {
    }
  }
}

Run the following command

It will create a new file layout-grid.tsx inside the components/mage-ui/image directory.

mkdir -p components/mage-ui/image && touch components/mage-ui/image/layout-grid.tsx

Paste the code

Open the newly created file and paste the following code:

"use client";
import React, { useState } from "react";
import { motion } from "framer-motion";
import { cn } from "@/lib/utils";
 
interface Card {
  id: number;
  content: JSX.Element | React.ReactNode | string;
  className: string;
  thumbnail: string;
}
 
interface LayoutGridProps {
  cards: Card[];
}
 
export const LayoutGrid: React.FC<LayoutGridProps> = ({ cards }) => {
  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 min-h-screen p-10 grid grid-cols-1 md:grid-cols-3 max-w-7xl mx-auto gap-6 relative">
      {cards.map((card, i) => (
        <div key={i} className={cn(card.className, "h-96")}>
          <motion.div
            onClick={() => handleClick(card)}
            className={cn(
              card.className,
              "relative overflow-hidden",
              selected?.id === card.id
                ? "rounded-lg cursor-pointer absolute inset-0 h-3/4 w-full md:w-3/4 m-auto z-50 flex justify-start items-start 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: React.FC<{ card: Card }> = ({ card }) => {
  return (
    <div className="relative h-full w-full">
      <motion.img
        layoutId={`image-${card.id}-image`}
        src={card.thumbnail}
        alt="thumbnail"
        className="object-cover object-top absolute inset-0 h-full w-full transition duration-200"
      />
    </div>
  );
};
 
const SelectedCard: React.FC<{ selected: Card }> = ({ selected }) => {
  return (
    <div className="bg-transparent h-full w-full flex flex-col justify-end rounded-lg shadow-2xl relative z-[60]">
      {/* Image component inside the selected card */}
      <motion.img
        layoutId={`image-${selected.id}-image`}
        src={selected.thumbnail}
        alt="thumbnail"
        className="object-cover object-top absolute inset-0 h-full w-full"
      />
      <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>
  );
};
 
export function LayoutGridDemo() {
  return (
    <div className="min-h-[260vh] py-20 min-w-[220vh]">
      <LayoutGrid cards={cards} />
    </div>
  );
}
 
const SkeletonOne = () => (
  <div>
    <p className="font-bold md:text-4xl text-xl text-white">House in the woods</p>
    <p className="font-normal text-base my-4 max-w-lg text-neutral-200">
      A serene and tranquil retreat, this house in the woods offers a peaceful escape from the hustle and bustle of city life.
    </p>
  </div>
);
 
const SkeletonTwo = () => (
  <div>
    <p className="font-bold md:text-4xl text-xl text-white">House above the clouds</p>
    <p className="font-normal text-base my-4 max-w-lg text-neutral-200">
      Perched high above the world, this house offers breathtaking views and a unique living experience.
    </p>
  </div>
);
 
const SkeletonThree = () => (
  <div>
    <p className="font-bold md:text-4xl text-xl text-white">Greens all over</p>
    <p className="font-normal text-base my-4 max-w-lg text-neutral-200">
      A house surrounded by greenery and nature's beauty. It's the perfect place to relax, unwind, and enjoy life.
    </p>
  </div>
);
 
const SkeletonFour = () => (
  <div>
    <p className="font-bold md:text-4xl text-xl text-white">Rivers are serene</p>
    <p className="font-normal text-base my-4 max-w-lg text-neutral-200">
      A house by the river is a place of peace and tranquility. It's the perfect place to relax and enjoy life.
    </p>
  </div>
);
 
const cards: Card[] = [
  { id: 1, content: <SkeletonOne />, className: "md:col-span-2", thumbnail: "https://images.unsplash.com/photo-1476231682828-37e571bc172f?q=80&w=3474&auto=format&fit=crop" },
  { id: 2, content: <SkeletonTwo />, className: "col-span-1", thumbnail: "https://images.unsplash.com/photo-1464457312035-3d7d0e0c058e?q=80&w=3540&auto=format&fit=crop" },
  { id: 3, content: <SkeletonThree />, className: "col-span-1", thumbnail: "https://images.unsplash.com/photo-1588880331179-bc9b93a8cb5e?q=80&w=3540&auto=format&fit=crop" },
  { id: 4, content: <SkeletonFour />, className: "md:col-span-2", thumbnail: "https://images.unsplash.com/photo-1475070929565-c985b496cb9f?q=80&w=3540&auto=format&fit=crop" }
];