Retour au catalogue

Bento Spotlight Sweep

Faisceau spotlight qui balaye la grille, illuminant les cartes a son passage. Effet cinematique.

bentocomplex Both Responsive a11y
darkboldelegantsaasagencygrid
Theme

How to build a bento grid with a mouse-tracking spotlight in React

A bento grid with a spotlight in React positions a single radial-gradient overlay absolutely over the grid container, then writes the cursor coordinates into CSS custom properties via Framer Motion useMotionValue. Every card beneath it gets the cinematic illumination pass without any per-card state.

  • Stack: React 18 + Framer Motion 11 + Tailwind v4 + Lucide React, ~100 lines.
  • Single overlay technique: one motion.div drives the spotlight for the entire grid, not one per card.
  • CSS custom properties (--mouse-x / --mouse-y) are bound directly to motion values, avoiding re-renders on every mouse move.
  • Accessible: content is readable without the overlay; the spotlight is purely decorative and pointer-events:none.
  • Mobile caveat: no pointer on touch screens, so the spotlight stays hidden and the grid renders cleanly without it.

Bento Spotlight Sweep is a feature-grid section where a single radial spotlight chases the cursor across a multi-column card layout. As the mouse sweeps across the grid, each card it passes over lights up with a soft accent glow, making the section feel alive. The effect is built with one overlay element, no per-card state, no scroll listeners, just Framer Motion motion values bound to CSS custom properties.

Anatomy

The component has three layers. First, an optional header block (badge, title, subtitle) fades in with a simple whileInView animation. Second, a relative grid container captures mousemove events and holds a ref for coordinate math. Third, inside that container sits a single absolute motion.div, the spotlight overlay, sized to cover the entire grid and driven by --mouse-x/--mouse-y custom properties. The cards themselves are standard motion.div elements that stagger in on scroll; they need no special spotlight logic.

How it works

The trick is attaching Framer Motion useMotionValue instances to CSS custom properties on the overlay element. On every mousemove, the handler reads the pointer position relative to the grid container via getBoundingClientRect, then calls mouseX.set() and mouseY.set(). The motion.div overlay reads --mouse-x and --mouse-y inside a radial-gradient background string. Because Framer Motion writes motion values directly to the DOM style without triggering a React re-render, the spotlight update is fully off the React cycle, smooth at 60fps even with many cards.

How to build it in React

  1. Set up the grid container with a ref and mousemove handler

    Create a div with position:relative, attach a ref to it, and wire an onMouseMove handler. Inside the handler, subtract the container's getBoundingClientRect() origin from the raw clientX/Y to get container-relative coordinates.

    const containerRef = React.useRef<HTMLDivElement>(null);
    const mouseX = useMotionValue(0);
    const mouseY = useMotionValue(0);
    
    function handleMouseMove(e: React.MouseEvent) {
      const rect = containerRef.current?.getBoundingClientRect();
      if (!rect) return;
      mouseX.set(e.clientX - rect.left);
      mouseY.set(e.clientY - rect.top);
    }
  2. Place the spotlight overlay inside the container

    Add a motion.div as the first child of the container with position:absolute, inset-0, z-index above the cards, and pointer-events:none. Bind the motion values to --mouse-x and --mouse-y via the style prop (Framer Motion accepts motion values as CSS custom properties).

    <motion.div
      className="pointer-events-none absolute inset-0 z-10 rounded-2xl opacity-60"
      style={{
        background: `radial-gradient(400px circle at var(--mouse-x) var(--mouse-y), color-mix(in srgb, var(--color-accent) 8%, transparent), transparent 60%)`,
        // @ts-expect-error CSS custom properties
        "--mouse-x": mouseX,
        "--mouse-y": mouseY,
      }}
    />
  3. Render the bento cards with staggered whileInView

    Map over your items array and render each as a motion.div with an initial opacity:0 / y:24 state, animating to visible on scroll. Use a small delay multiplier (i * 0.08) for the cascade. The spotlight overlay sits above the cards via z-index and illuminates them purely through the radial gradient blend.

    items.map((item, i) => (
      <motion.div
        key={item.id}
        initial={{ opacity: 0, y: 24 }}
        whileInView={{ opacity: 1, y: 0 }}
        viewport={{ once: true, margin: "-40px" }}
        transition={{ delay: i * 0.08, duration: 0.5 }}
        className="relative rounded-2xl border p-6"
      />
    ))
  4. Theme the spotlight color with a CSS token

    Replace any hardcoded color with var(--color-accent) inside the radial-gradient string. Adjust the alpha via color-mix (8% works well on both light and dark themes). To make the beam wider or narrower, change the 400px radius value, larger values create a softer, more ambient glow.

When to use it

Reach for this section on SaaS feature pages and agency portfolios where you want the features grid to feel interactive rather than static. It works best on dark or bold themes where the accent glow creates visible contrast. Skip it on text-heavy pages where the moving light competes with reading, and always verify that the grid looks good without the spotlight on mobile, the layout should stand on its own.

Used by

  • Vercel, Uses cursor-reactive radial highlights across its feature and infrastructure sections.
  • Linear, Spotlight glow effects on feature grids give the product site its distinct polished feel.
  • Resend, Bento-style feature layout with ambient light effects highlighting card edges on hover.

FAQ

Why use CSS custom properties instead of inline style with a template string?

Framer Motion can write motion values directly to CSS custom properties on a DOM element, bypassing React's reconciler entirely. This means the radial-gradient string is never re-evaluated by React on each frame, the browser just reads the updated --mouse-x/--mouse-y values, keeping the animation thread-safe and jank-free.

Does the spotlight work when the grid scrolls off screen?

The mousemove listener is on the grid container itself, so it only fires when the cursor is inside the grid bounds. When the user scrolls away, no events fire and the spotlight stays at its last position, invisible because the container is out of the viewport.

How do I adjust the spotlight radius and intensity?

Change the 400px in the radial-gradient string to make the circle larger or smaller. Raise or lower the color-mix percentage (default 8%) to increase or decrease the glow intensity. A value between 6% and 15% works well; beyond 20% the effect starts to look harsh rather than atmospheric.

Can I add a per-card border highlight on top of the global spotlight?

Yes. Track the pointer position relative to each card individually (a second mousemove on each card div or a single listener that computes per-card offsets), then set a CSS custom property on that card to drive a border-image or box-shadow. This pairs well with the global overlay to create a layered depth effect.

"use client";

import React from "react";
import { motion, useMotionValue } from "framer-motion";
import * as LucideIcons from "lucide-react";

interface BentoItem {
  id: string;
  title: string;
  description: string;
  icon?: string;
}

interface BentoSpotlightSweepProps {
  badge?: string;
  title?: string;
  subtitle?: string;
  items: BentoItem[];
}

function getIcon(name?: string) {
  if (!name) return null;

Code complet réservé à Pro

Code source intégral, export multi-framework et playground.

Passer en Pro, 9,99€/mois

Reviews

React Bento Grid with Mouse Spotlight Effect, Tutorial