Magic Bento | Pointer-aware Information Grid
Magic Bento combines information cards, pointer position, local glow feedback, and a pinned selection state into a lightweight React interaction case.
Effect Showcase
Design Notes
Scenario
A bento grid is often treated as visual layout, but its interaction value is helping users understand a set of entry points. A static card list can show titles and descriptions. Magic Bento turns pointer position into local feedback, so the interface responds before the user commits.
This belongs in the Lab because it is not a full design-system component and not a large product feature. It is a small behavior: scan a set of options, inspect one card, then click to confirm.
Interaction Design
Magic Bento has three layers of state:
- Pointer focus
When the pointer enters a card, only that card receives local radial feedback, so the grid does not become noisy.
- Selected intent
Clicking a card preserves the selected state and turns exploration into a decision.
- Quiet density
The grid stays compact, but every card has a title, index, explanation, and action signal.
Implementation Model
The minimal version does not need Framer Motion or another animation runtime. React stores only the current card-local pointer position and the selected card id. Each card uses getBoundingClientRect() inside onPointerMove to calculate local coordinates, then writes those coordinates into an inline radial-gradient().
The styling uses existing Tailwind utilities and site theme tokens. The glow is not a custom Tailwind class and does not require config changes; it is just a style.background value.
SEO and Content Positioning
The useful keywords are not only "animated card". They are "pointer-aware card", "bento grid", and "React interaction case". The case explains how visual feedback, accessible buttons, selected state, and responsive layout can fit into one reusable pattern.
Testing Notes
Test four boundaries: the grid does not overflow on mobile, keyboard focus reaches every card, aria-pressed matches the visible selected state, and the glow layer does not intercept clicks or resize the layout.
Copyable Source
Source is read from the case examples folder, so the displayed snippet and typechecked code stay aligned.
"use client"; import * as React from "react"; const bentoItems = [ { id: "intent", title: "Intent", value: "01", body: "Start with the user action, not the visual effect.", }, { id: "feedback", title: "Feedback", value: "02", body: "The hovered card explains what will respond before selection.", }, { id: "boundary", title: "Boundary", value: "03", body: "Pointer state follows the nearest card, so gaps keep the glow alive.", }, { id: "motion", title: "Motion", value: "04", body: "Keep animation local and readable so the glow supports attention.", }, { id: "density", title: "Density", value: "05", body: "Use compact rhythm to show more choices while each item stays clear.", }, { id: "decision", title: "Decision", value: "06", body: "Clicking pins the chosen card and turns exploration into a choice.", },]; const glowRadius = 180;const edgeActivationDistance = 24;const hoverGlowColor = "rgb(var(--lab-hover-purple-rgb) / 0.28)";const borderBeamColor = "rgb(var(--lab-hover-purple-rgb) / 0.72)";const borderBeamMaskStyle: React.CSSProperties = { padding: 1, WebkitMask: "linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)", WebkitMaskComposite: "xor", maskComposite: "exclude",}; type PointerProjection = { x: number; y: number; width: number; height: number; distance: number;}; type PointerState = { ownerId: string | null; projections: Record<string, PointerProjection>;} | null; function distanceToRect(x: number, y: number, rect: DOMRect) { const dx = x < rect.left ? rect.left - x : x > rect.right ? x - rect.right : 0; const dy = y < rect.top ? rect.top - y : y > rect.bottom ? y - rect.bottom : 0; return Math.sqrt(dx * dx + dy * dy);} function isNearBorder(projection: PointerProjection | undefined) { if (!projection) return false; const { height, width, x, y } = projection; const isWithinHorizontalRange = x >= -edgeActivationDistance && x <= width + edgeActivationDistance; const isWithinVerticalRange = y >= -edgeActivationDistance && y <= height + edgeActivationDistance; return ( (isWithinHorizontalRange && Math.abs(y) <= edgeActivationDistance) || (isWithinHorizontalRange && Math.abs(y - height) <= edgeActivationDistance) || (isWithinVerticalRange && Math.abs(x) <= edgeActivationDistance) || (isWithinVerticalRange && Math.abs(x - width) <= edgeActivationDistance) );} function borderBeamStyle( projection: PointerProjection | undefined,): React.CSSProperties { return { ...borderBeamMaskStyle, background: projection && isNearBorder(projection) ? `radial-gradient(92px circle at ${projection.x}px ${projection.y}px, ${borderBeamColor}, transparent 68%)` : "transparent", };} function projectedPointerState( event: React.PointerEvent<HTMLElement>, elements: Map<string, HTMLElement>,): PointerState { const ownerElement = event.target instanceof Element ? event.target.closest<HTMLElement>("[data-magic-bento-item]") : null; const ownerId = ownerElement && event.currentTarget.contains(ownerElement) ? (ownerElement.dataset.magicBentoItem ?? null) : null; const projections: Record<string, PointerProjection> = {}; for (const [id, element] of elements) { const rect = element.getBoundingClientRect(); projections[id] = { distance: distanceToRect(event.clientX, event.clientY, rect), height: rect.height, width: rect.width, x: event.clientX - rect.left, y: event.clientY - rect.top, }; } if (!Object.keys(projections).length) return null; return { ownerId, projections };} export function MagicBentoGrid() { const cardRefs = React.useRef(new Map<string, HTMLElement>()); const [pointer, setPointer] = React.useState<PointerState>(null); const [selectedId, setSelectedId] = React.useState(bentoItems[0].id); const setCardRef = React.useCallback( (id: string, element: HTMLElement | null) => { if (element) { cardRefs.current.set(id, element); return; } cardRefs.current.delete(id); }, [], ); const handlePointerMove = React.useCallback( (event: React.PointerEvent<HTMLDivElement>) => { setPointer(projectedPointerState(event, cardRefs.current)); }, [], ); return ( <div className="rounded-card border border-border bg-background p-[48px] shadow-soft"> <div aria-label="Magic bento cards" className="-m-1 grid grid-cols-1 gap-0 md:grid-cols-2 lg:grid-cols-3" onPointerLeave={() => setPointer(null)} onPointerMove={handlePointerMove} role="group" > {bentoItems.map((item) => { const projection = pointer?.projections[item.id]; const isLit = Boolean( projection && projection.distance <= glowRadius, ); const isSelected = selectedId === item.id; const glowStyle: React.CSSProperties = { background: isLit ? `radial-gradient(${glowRadius}px circle at ${projection!.x}px ${projection!.y}px, ${hoverGlowColor}, transparent 68%)` : "transparent", }; const beamStyle = borderBeamStyle(projection); return ( <button aria-pressed={isSelected} className="group h-full min-w-0 rounded-card p-1 text-left outline-none" data-magic-bento-item={item.id} key={item.id} onClick={() => setSelectedId(item.id)} type="button" > <span className="relative flex h-full min-h-[132px] flex-col overflow-hidden rounded-card border border-border bg-card p-4 transition-colors group-focus-visible:ring-2 group-focus-visible:ring-ring" data-magic-bento-card="" ref={(element) => setCardRef(item.id, element)} > <span aria-hidden="true" className="pointer-events-none absolute inset-0 transition-opacity duration-200" data-magic-bento-glow="" style={glowStyle} /> <span aria-hidden="true" className="pointer-events-none absolute inset-0 rounded-card transition-opacity duration-200" data-magic-bento-border-beam="" style={beamStyle} /> <span className="relative z-10 flex h-full flex-1 flex-col justify-between gap-8"> <span className="flex items-center justify-between gap-3"> <span className="text-sm font-semibold text-foreground"> {item.title} </span> <span className="font-mono text-xs text-muted-foreground"> {item.value} </span> </span> <span className="text-sm leading-6 text-muted-foreground"> {item.body} </span> <span className="text-xs font-medium text-primary"> {isSelected ? "Selected" : "Inspect"} </span> </span> </span> </button> ); })} </div> </div> );} export default MagicBentoGrid;Agent Prompt
Agent Prompt
Build a Magic Bento interaction inspired by https://reactbits.dev/components/magic-bento, but implement the simplified Lab version with only React, browser pointer events, inline radial gradients, and existing Tailwind utility classes. Do not install animation libraries, do not add Tailwind plugins, and do not change Tailwind config. Deliver one self-contained React component, a small preview, and localized Markdown content. The interaction should show a compact bento grid where pointer movement creates a local card glow, clicking a card pins the selected state, and the visual design remains quiet enough for an engineering blog. Use semantic buttons, `aria-pressed`, theme tokens, and a mobile-safe grid. Acceptance: the grid fits mobile and desktop, the glow follows the hovered card only, the selected state is visible without relying only on color, the example source is the same component used by the demo, and the implementation stays understandable without external animation dependencies.