Bento Grid
A responsive masonry-style grid layout with magnetic spotlight effects.
Overview
A flexible grid system for creating Apple-style bento layouts with interactive cards. Each card features cursor-tracking spotlight effects and smooth hover animations.
Features
- Responsive grid with automatic row sizing
- Magnetic spotlight that follows cursor
- Spring-based hover lift animations
- Customizable card spans and layouts
- Viewport-aware entrance animations
Preview
Feature One
Description here
Feature Two
Another feature
Installation
Method 1: CLI (Recommended)
Install via the ZenBlocks CLI to automatically handle dependencies:
npx shadcn@latest add https://zenblocks-three.vercel.app/r/bento-grid.jsonMethod 2: Manual
-
Install Dependencies
npm install framer-motion lucide-react clsx tailwind-merge -
Copy the Source Code
Copy the code below into
components/zenblocks/bento-grid.tsx.Click to expand source
"use client"; import React from "react"; import { motion, useMotionValue, useSpring, useTransform } from "framer-motion"; import { ArrowRight, Box } from "lucide-react"; import { cn } from "@/lib/utils"; const MotionDiv = motion.div as any; /* -------------------------------------------------------------------------- */ /* BENTOCARD */ /* -------------------------------------------------------------------------- */ export interface BentoCardProps { children?: React.ReactNode; className?: string; /** Standard Props for Simple Mode */ title?: string; description?: React.ReactNode; icon?: React.ReactNode; visual?: React.ReactNode; /** Animation Delay */ delay?: number; /** Href for making the card a link */ href?: string; } /** * BentoCard: A versatile card with magnetic spotlight and spring/hover effects. * * Usage Modes: * 1. Simple: Pass `title`, `description`, `icon`, `visual`. * 2. Custom: Pass `children` to render arbitrary content inside the spotlight shell. */ export const BentoCard = ({ children, className = "", title, description, icon, visual, delay = 0, href }: BentoCardProps) => { const mouseX = useMotionValue(0); const mouseY = useMotionValue(0); const handleMouseMove = ({ currentTarget, clientX, clientY }: React.MouseEvent) => { const { left, top } = currentTarget.getBoundingClientRect(); mouseX.set(clientX - left); mouseY.set(clientY - top); }; const smoothX = useSpring(mouseX, { damping: 35, stiffness: 350 }); const smoothY = useSpring(mouseY, { damping: 35, stiffness: 350 }); const Container = href ? (motion.a as any) : MotionDiv; return ( <Container href={href} initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-50px" }} whileHover={{ y: -8 }} transition={{ y: { type: "spring", stiffness: 200, damping: 20 }, opacity: { duration: 0.8, delay, ease: [0.16, 1, 0.3, 1] } }} onMouseMove={handleMouseMove} className={cn( "group relative flex flex-col overflow-visible rounded-[2rem] transition-all duration-500 text-left h-full", href && "cursor-pointer", className )} > {/* Dynamic Background Spotlight (Shared Logic) */} <div className={cn( "relative z-10 flex flex-col flex-1 overflow-hidden rounded-[2rem] bg-white/80 dark:bg-zinc-950/80 backdrop-blur-2xl border border-zinc-200 dark:border-zinc-800 transition-all duration-700 h-full", "group-hover:border-zinc-400 dark:group-hover:border-zinc-100/30", "group-hover:shadow-[0_48px_96px_-24px_rgba(0,0,0,0.15)] dark:group-hover:shadow-[0_48px_96px_-24px_rgba(255,255,255,0.03)]" )}> {/* Spotlights */} <MotionDiv className="pointer-events-none absolute -inset-px z-10 opacity-0 transition-opacity duration-1000 group-hover:opacity-100" style={{ background: useTransform( [smoothX, smoothY], ([x, y]: any[]) => `radial-gradient(400px circle at ${x}px ${y}px, rgba(0,0,0,0.03), transparent 70%)` ), }} /> <MotionDiv className="pointer-events-none absolute -inset-px z-10 opacity-0 transition-opacity duration-1000 group-hover:dark:opacity-100 hidden dark:block" style={{ background: useTransform( [smoothX, smoothY], ([x, y]: any[]) => `radial-gradient(400px circle at ${x}px ${y}px, rgba(255,255,255,0.04), transparent 70%)` ), }} /> {/* Main Content Body */} <div className="relative z-20 flex-1 flex flex-col h-full"> {children ? ( // Custom Mode: Render children directly children ) : ( // Simple Mode: Render standard layout <div className="flex flex-col h-full p-8 pt-12"> {/* Visual Placeholder Area */} <div className="flex-1 w-full overflow-hidden rounded-xl mb-6 border border-zinc-200/40 dark:border-zinc-800/40 bg-zinc-100/30 dark:bg-zinc-900/30 relative flex items-center justify-center min-h-[140px]"> {visual || ( <div className="flex flex-col items-center gap-2 opacity-20"> <Box size={32} /> </div> )} <div className="absolute inset-0 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity duration-1000 bg-gradient-to-tr from-transparent via-white/[0.03] to-transparent -translate-x-full group-hover:translate-x-full transition-transform ease-in-out" /> </div> {/* Labeling & Iconography */} <div className="flex items-center gap-5 text-left mt-auto"> <div className="p-3.5 rounded-2xl bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 shrink-0 shadow-sm text-zinc-500 group-hover:bg-zinc-900 group-hover:text-white dark:group-hover:bg-white dark:group-hover:text-zinc-950 transition-all duration-500"> {icon || <ArrowRight size={14} />} </div> <div className="space-y-1 overflow-hidden"> <h3 className="text-[13px] font-black text-zinc-900 dark:text-zinc-100 uppercase tracking-tight truncate"> {title} </h3> <div className="text-[11px] leading-relaxed text-zinc-400 dark:text-zinc-500 line-clamp-1 font-medium opacity-80"> {description} </div> </div> </div> </div> )} </div> </div> </Container> ); }; /* -------------------------------------------------------------------------- */ /* COMPOUND COMPONENTS */ /* -------------------------------------------------------------------------- */ export const BentoCardTitle = ({ children, className }: { children: React.ReactNode; className?: string }) => ( <h3 className={cn("text-base font-bold text-zinc-900 dark:text-zinc-100", className)}> {children} </h3> ); export const BentoCardDescription = ({ children, className }: { children: React.ReactNode; className?: string }) => ( <p className={cn("text-sm text-zinc-500 dark:text-zinc-400 leading-relaxed", className)}> {children} </p> ); export const BentoCardContent = ({ children, className }: { children: React.ReactNode; className?: string }) => ( <div className={cn("p-6 flex-1", className)}> {children} </div> ); // Namespace Attachments (BentoCard as any).Title = BentoCardTitle; (BentoCard as any).Description = BentoCardDescription; (BentoCard as any).Content = BentoCardContent; /* -------------------------------------------------------------------------- */ /* BENTOGRID */ /* -------------------------------------------------------------------------- */ export interface BentoGridProps { children: React.ReactNode; className?: string; } export const BentoGrid: React.FC<BentoGridProps> = ({ children, className }) => { return ( <div className={cn( "grid grid-cols-1 md:grid-cols-3 gap-8 md:auto-rows-[22rem]", className )}> {children} </div> ); };
Usage
Simple Mode
Use props for a quick, standardized layout.
import { BentoGrid, BentoCard } from "@/components/zenblocks/bento-grid";
import { Zap } from "lucide-react";
<BentoGrid>
<BentoCard
title="Fast Performance"
description="Optimized for speed"
icon={<Zap size={14} />}
className="md:col-span-2"
/>
</BentoGrid>Custom Content Integration
The real power of BentoCard is its ability to wrap anything—images, forms, charts, or custom UI blocks.
Image Card (Edge-to-Edge)
<BentoCard className="md:col-span-1 md:row-span-2">
<div className="relative w-full h-full">
<img
src="/my-gallery-image.jpg"
alt="Gallery"
className="absolute inset-0 w-full h-full object-cover"
/>
<div className="absolute bottom-0 left-0 p-6 bg-black/50 w-full backdrop-blur-sm">
<h3 className="text-white font-bold">Photography</h3>
</div>
</div>
</BentoCard>Custom Interactive Block
<BentoCard className="md:col-span-2">
<div className="p-6 flex flex-col h-full bg-indigo-500/10">
<h3 className="text-xl font-bold mb-4">Interactive Widget</h3>
<div className="flex-1 bg-white/5 rounded-lg border border-white/10 p-4">
{/* Your custom component here */}
<MyChartComponent />
</div>
</div>
</BentoCard>Props
BentoGrid
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | - | BentoCard components |
| className | string | - | Additional CSS classes |
BentoCard
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | - | New & Recommended: Custom content. If present, standard props (title, etc.) are ignored. |
| title | string | - | Card heading text (Simple Mode) |
| description | ReactNode | - | Card description (Simple Mode) |
| icon | ReactNode | - | Icon component to display (Simple Mode) |
| visual | ReactNode | - | Custom visual content area (Simple Mode) |
| delay | number | 0 | Entrance animation delay |
| href | string | - | If set, the entire card becomes a link |
| className | string | - | Grid span and styling classes |
Ease of Use
The new BentoGrid is designed to be both user-friendly and professional:
- Simple? Just pass a title and description.
- Complex? Pass your own components as children.
- Interactive? Add an
hrefto turn the whole card into a link.
Accessibility
- Semantic heading structure with
<h3>for titles - Keyboard focusable cards
- Proper contrast ratios in light and dark modes
- Motion animations respect
prefers-reduced-motion
Customization
Grid Spans
<BentoCard
className="md:col-span-2 md:row-span-2"
title="Large Feature"
description="Takes more space"
/>Custom Visual
<BentoCard
visual={<YourComponent />}
title="Custom Content"
description="With your own visuals"
/>Motion Behavior
- Spotlight: Radial gradient follows cursor position using
useMotionValueanduseTransform - Hover lift: Cards translate -8px on Y-axis with spring physics
- Entrance: Staggered fade-in with upward motion based on
delayprop
Performance Notes
- Spotlight uses CSS
backgroundproperty updated via motion values (GPU-accelerated) - Viewport intersection observer triggers entrance animations once
- Grid uses CSS Grid for efficient layout calculations
Examples
Feature Grid
<BentoGrid>
<BentoCard className="md:col-span-2 md:row-span-2" {...largeFeature} />
<BentoCard {...smallFeature1} />
<BentoCard {...smallFeature2} />
<BentoCard className="md:col-span-3" {...wideFeature} />
</BentoGrid>Notes
- Default grid is 1 column on mobile, 3 columns on desktop
- Auto-rows are set to 22rem for consistent card heights
- Spotlight effect is more visible in dark mode