Component

Interactive Bento Grid

A modern bento-style layout that turns a simple grid into an experience. Each tile responds to the cursor — lifting gently, casting a soft spotlight, and leaning toward your pointer — so the whole section feels alive and premium. Mix large and small tiles to highlight what matters, and it reflows beautifully from desktop down to mobile.

Features

  • Interactive tiles : Every tile reacts to the cursor with a subtle lift and glow, making the layout feel tactile and engaging rather than static.
  • Cursor-following spotlight : A soft light trails the pointer across each tile, drawing the eye and adding depth and polish.
  • Magnetic hover : Tiles gently lean toward the cursor and spring back, a delightful micro-interaction that invites people to explore.
  • Flexible bento layout : Freely mix wide, tall, and standard tiles to build a balanced, editorial arrangement that guides attention to your key content.
  • Responsive by design : The grid rebalances smoothly across desktop, tablet, and mobile, staying clean and readable at every screen size.
Spark UI

Components that feel alive.

Copy-paste, prop-driven, built with React, Tailwind and Motion.

Developers

0+

shipping with Spark UI

Live
Mayadeployed to prod
Arjunopened PR #214
Lenastarred the repo
Theomerged main
Spotlight

A glow follows your cursor across every tile.

Built with
React
Tailwind
Motion
TypeScript

Zero config. Copy-paste.

Browse all components →

60+ and counting. Dropping weekly.

Installation

01.Install Dependencies

npm install motion
Source Code
'use client';
import { motion, useMotionTemplate, useMotionValue, useSpring } from 'motion/react';
import { cn } from '@/utils/cn';

// Static Tailwind classes so the JIT compiler always picks them up.
const GRID_COLS = {
  1: 'lg:grid-cols-1',
  2: 'lg:grid-cols-2',
  3: 'lg:grid-cols-3',
  4: 'lg:grid-cols-4',
};

const COL_SPAN = {
  1: '',
  2: 'lg:col-span-2',
  3: 'lg:col-span-3',
  4: 'lg:col-span-4',
};

const ROW_SPAN = {
  1: '',
  2: 'lg:row-span-2',
  3: 'lg:row-span-3',
};

export function BentoGrid({ className, children, columns = 3, ...props }) {
  return (
    <div
      className={cn('grid grid-cols-1 gap-4 sm:grid-cols-2', GRID_COLS[columns], className)}
      {...props}
    >
      {children}
    </div>
  );
}

const MAGNETIC_MAX = 26; // px cap, keeps tiles from drifting too far

export function BentoTile({
  className,
  children,
  colSpan = 1,
  rowSpan = 1,
  spotlight = true,
  spotlightColor = 'rgba(255,255,255,0.10)',
  hoverLift = true,
  magnetic = false,
  magneticStrength = 0.18,
  onMouseMove,
  onMouseLeave,
  ...props
}) {
  // Spotlight position (px from the tile's top-left).
  const mx = useMotionValue(0);
  const my = useMotionValue(0);
  const spotlightBg = useMotionTemplate`radial-gradient(220px circle at ${mx}px ${my}px, ${spotlightColor}, transparent 70%)`;

  // Magnetic translate — a spring chases the pulled-toward-cursor target.
  const x = useSpring(0, { stiffness: 220, damping: 18, mass: 0.4 });
  const y = useSpring(0, { stiffness: 220, damping: 18, mass: 0.4 });

  const handleMove = (e) => {
    const r = e.currentTarget.getBoundingClientRect();
    mx.set(e.clientX - r.left);
    my.set(e.clientY - r.top);

    if (magnetic) {
      const pull = (offset) =>
        Math.max(-MAGNETIC_MAX, Math.min(MAGNETIC_MAX, offset * magneticStrength));
      x.set(pull(e.clientX - (r.left + r.width / 2)));
      y.set(pull(e.clientY - (r.top + r.height / 2)));
    }
    onMouseMove?.(e);
  };

  const handleLeave = (e) => {
    x.set(0);
    y.set(0);
    onMouseLeave?.(e);
  };

  // Magnetic tiles already move via x/y, so the hover effect is scale-only.
  const hover = !hoverLift ? undefined : magnetic ? { scale: 1.03 } : { y: -4, scale: 1.01 };

  return (
    <motion.div
      onMouseMove={handleMove}
      onMouseLeave={handleLeave}
      whileHover={hover}
      transition={{ type: 'spring', stiffness: 300, damping: 24 }}
      style={magnetic ? { x, y } : undefined}
      className={cn(
        'group relative overflow-hidden rounded-2xl border border-white/10 bg-zinc-900/40 p-6',
        COL_SPAN[colSpan],
        ROW_SPAN[rowSpan],
        props.onClick && 'cursor-pointer',
        className
      )}
      {...props}
    >
      {spotlight && (
        <motion.div
          aria-hidden
          className="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-300 group-hover:opacity-100"
          style={{ background: spotlightBg }}
        />
      )}
      <div className="relative h-full">{children}</div>
    </motion.div>
  );
}
sparkui