Image expand

A stack of images that opens into a playful spatial layout.

Source code

Copy and paste into your project
MultipleImageExpand.tsx
"use client";
 
import Image from "next/image";
import { motion } from "framer-motion";
import { useMemo, useState } from "react";
 
type ImageType = {
src: string;
alt: string;
};
 
type ImageStackProps = {
images: ImageType[];
className?: string;
};
 
const expandedOffsets = [
{ x: -96, y: -54 },
{ x: 96, y: -54 },
{ x: -78, y: 54 },
{ x: 78, y: 54 },
];
 
function rotationForIndex(index: number, expanded = false) {
const rotations = [-10, 7, -4, 11];
const base = rotations[index] ?? 0;
return expanded ? base * 1.2 : base * 0.7;
}
 
export default function MultipleImageExpand({ images, className = "" }: ImageStackProps) {
const [isExpanded, setIsExpanded] = useState(false);
const rotations = useMemo(
() => images.map((_, index) => ({ collapsed: rotationForIndex(index), expanded: rotationForIndex(index, true) })),
[images],
);
 
return (
<div className={`relative flex justify-center ${className}`}>
<div className="relative h-[260px] w-[440px]">
{images.map((image, index) => {
const offset = isExpanded ? expandedOffsets[index] ?? { x: 0, y: 0 } : { x: 0, y: index * 13 };
 
return (
<motion.button
key={image.src}
type="button"
aria-label={`${isExpanded ? "Collapse" : "Expand"} image stack`}
aria-pressed={isExpanded}
className="absolute left-1/2 top-1/2 h-[126px] w-[190px] origin-center -translate-x-1/2 -translate-y-1/2 cursor-pointer overflow-hidden rounded-xl border-[3px] border-white bg-white p-0 shadow-md outline-none focus-visible:ring-2 focus-visible:ring-neutral-300 dark:border-neutral-700 dark:bg-neutral-800 dark:focus-visible:ring-neutral-600"
initial={false}
animate={{
x: offset.x,
y: offset.y,
rotate: isExpanded ? rotations[index].expanded : rotations[index].collapsed,
scale: isExpanded ? 1.015 : 1,
zIndex: isExpanded ? 20 + index : 10 + index,
}}
whileHover={{ scale: isExpanded ? 1.04 : 1.025 }}
whileTap={{ scale: 0.98 }}
transition={{ type: "spring", stiffness: 180, damping: 24, mass: 0.75 }}
onClick={() => setIsExpanded((value) => !value)}
>
<Image src={image.src} alt={image.alt} fill sizes="190px" className="object-cover" priority />
</motion.button>
);
})}
</div>
</div>
);
}