Please use Desktop to view and interact with components
Decorative600msspring
Reactive Background Grid
Background pattern that reacts to mouse movement and click ripples
gridbackgroundreactiverippleinteractive
Useto navigate between components
Preview
Code
TypeScript + React
'use client'
import { useState, MouseEvent } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
export function ReactiveBackgroundGrid() {
const [ripples, setRipples] = useState<Array<{ id: number; x: number; y: number }>>([])
const [hoveredDot, setHoveredDot] = useState<{ x: number; y: number } | null>(null)
const [containerSize, setContainerSize] = useState({ width: 400, height: 400 })
const density = 20
const handleClick = (e: MouseEvent<HTMLDivElement>) => {
const rect = e.currentTarget.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
const newRipple = { id: Date.now(), x, y }
setRipples((prev) => [...prev, newRipple])
setTimeout(() => {
setRipples((prev) => prev.filter((r) => r.id !== newRipple.id))
}, 600)
}
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
const rect = e.currentTarget.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
setHoveredDot({ x, y })
setContainerSize({ width: rect.width, height: rect.height })
}
const gridSize = density
const dotsArray = Array.from({ length: gridSize * gridSize })
return (
<div
onClick={handleClick}
onMouseMove={handleMouseMove}
onMouseLeave={() => setHoveredDot(null)}
className="relative h-96 w-full overflow-hidden rounded-2xl border border-border bg-card"
>
<div
className="absolute inset-0"
style={{
backgroundImage: 'radial-gradient(circle, currentColor 1px, transparent 1px)',
backgroundSize: `${100 / gridSize}% ${100 / gridSize}%`,
backgroundPosition: '0 0',
color: 'rgba(99, 102, 241, 0.1)',
}}
/>
{dotsArray.map((_, index) => {
const row = Math.floor(index / gridSize)
const col = index % gridSize
const x = (col / gridSize) * 100
const y = (row / gridSize) * 100
const distance = hoveredDot
? Math.sqrt(
Math.pow(x - (hoveredDot.x / containerSize.width) * 100, 2) +
Math.pow(y - (hoveredDot.y / containerSize.height) * 100, 2)
)
: Infinity
const scale = hoveredDot && distance < 30 ? 1.5 : 1
const opacity = hoveredDot && distance < 30 ? 0.6 : 0.2
return (
<motion.div
key={index}
animate={{ scale, opacity }}
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
className="absolute rounded-full bg-primary"
style={{
left: `${x}%`,
top: `${y}%`,
width: '4px',
height: '4px',
transform: 'translate(-50%, -50%)',
}}
/>
)
})}
<AnimatePresence>
{ripples.map((ripple) => (
<motion.div
key={ripple.id}
initial={{ scale: 0, opacity: 0.8 }}
animate={{ scale: 4, opacity: 0 }}
exit={{ scale: 4, opacity: 0 }}
transition={{ duration: 0.6 }}
className="absolute rounded-full border-2 border-primary"
style={{
left: ripple.x,
top: ripple.y,
width: '100px',
height: '100px',
transform: 'translate(-50%, -50%)',
}}
/>
))}
</AnimatePresence>
</div>
)
}
How to Use
- 1Install Framer Motion:
npm install framer-motion - 2Copy the code from above
- 3Paste it into your project and customize as needed
- 4Colors are customizable via Tailwind CSS classes. The default theme uses dark mode colors defined in your globals.css file