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 motionSource 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>
);
}