Bento Grid
Asymmetric responsive grid with hover-expand animation and spotlight glow
layoutgridbentoresponsiveinteractive
Install dependencies
$npm install framer-motionPreview
Source Code
"use client";
import { useRef, useState } from "react";
import { motion } from "framer-motion";
interface BentoGridProps {
children: React.ReactNode;
columns?: number;
className?: string;
}
interface BentoGridItemProps {
children: React.ReactNode;
colSpan?: number;
rowSpan?: number;
className?: string;
}
export function BentoGrid({
children,
columns = 3,
className = "",
}: BentoGridProps) {
return (
<div
className={`grid gap-4 ${className}`}
style={{
gridTemplateColumns: `repeat(${columns}, 1fr)`,
gridAutoRows: "minmax(140px, auto)",
}}
>
{children}
</div>
);
}
export function BentoGridItem({
children,
colSpan = 1,
rowSpan = 1,
className = "",
}: BentoGridItemProps) {
const ref = useRef<HTMLDivElement>(null);
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
const [isHovered, setIsHovered] = useState(false);
const handleMouseMove = (e: React.MouseEvent) => {
if (!ref.current) return;
const rect = ref.current.getBoundingClientRect();
setMousePos({
x: e.clientX - rect.left,
y: e.clientY - rect.top,
});
};
return (
<motion.div
ref={ref}
onMouseMove={handleMouseMove}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
whileHover={{ scale: 1.02 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
className={`relative overflow-hidden rounded-2xl border border-white/15 bg-gray-900 p-6 ${className}`}
style={{
gridColumn: `span ${colSpan}`,
gridRow: `span ${rowSpan}`,
}}
>
{/* Spotlight glow */}
<div
className="pointer-events-none absolute -inset-px rounded-2xl transition-opacity duration-300"
style={{
opacity: isHovered ? 1 : 0,
background: `radial-gradient(400px circle at ${mousePos.x}px ${mousePos.y}px, rgba(56,189,248,0.12), transparent 40%)`,
}}
/>
<div className="relative z-10">{children}</div>
</motion.div>
);
}
export default BentoGrid;