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

Live Preview

Feature One

Description here

Feature Two

Another feature

Installation

Install via the ZenBlocks CLI to automatically handle dependencies:

npx shadcn@latest add https://zenblocks-three.vercel.app/r/bento-grid.json

Method 2: Manual

  1. Install Dependencies

    npm install framer-motion lucide-react clsx tailwind-merge
  2. 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

PropTypeDefaultDescription
childrenReactNode-BentoCard components
classNamestring-Additional CSS classes

BentoCard

PropTypeDefaultDescription
childrenReactNode-New & Recommended: Custom content. If present, standard props (title, etc.) are ignored.
titlestring-Card heading text (Simple Mode)
descriptionReactNode-Card description (Simple Mode)
iconReactNode-Icon component to display (Simple Mode)
visualReactNode-Custom visual content area (Simple Mode)
delaynumber0Entrance animation delay
hrefstring-If set, the entire card becomes a link
classNamestring-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 href to 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 useMotionValue and useTransform
  • Hover lift: Cards translate -8px on Y-axis with spring physics
  • Entrance: Staggered fade-in with upward motion based on delay prop

Performance Notes

  • Spotlight uses CSS background property 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