effects#animation#interactive
Grid Distortion
A dot grid that warps and distorts based on cursor proximity
Implementation Guide
# Grid Distortion Effect
> A dot grid that warps and distorts based on cursor proximity. Creates an interactive "magnetic" effect that responds to mouse movement.
## Quick Start
```tsx
import GridDistortion from '@/components/effects/grid-distortion';
export default function InteractiveSection() {
return (
<div className="relative h-[500px]">
<GridDistortion dotColor="#007AFF" />
<div className="relative z-10 flex h-full items-center justify-center">
<h1 className="text-4xl font-bold text-white">Interactive Grid</h1>
</div>
</div>
);
}
```
## Props
| Prop | Type | Default | Description |
| --------------- | ------ | --------- | ------------------------------------ |
| gridSize | number | 30 | Spacing between dots in pixels |
| dotSize | number | 3 | Diameter of each dot in pixels |
| dotColor | string | '#007AFF' | Color of the dots (hex or CSS color) |
| maxDisplacement | number | 20 | Maximum pixel distance dots can move |
| influenceRadius | number | 150 | Radius of mouse influence in pixels |
## Full Implementation
```tsx
'use client';
import { useEffect, useRef, useState } from 'react';
interface GridDistortionProps {
gridSize?: number;
dotSize?: number;
dotColor?: string;
maxDisplacement?: number;
influenceRadius?: number;
}
export default function GridDistortion({
gridSize = 30,
dotSize = 3,
dotColor = '#007AFF',
maxDisplacement = 20,
influenceRadius = 150,
}: GridDistortionProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [mousePos, setMousePos] = useState({ x: -1000, y: -1000 });
const [dots, setDots] = useState<{ x: number; y: number }[]>([]);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const rect = container.getBoundingClientRect();
const cols = Math.ceil(rect.width / gridSize) + 1;
const rows = Math.ceil(rect.height / gridSize) + 1;
const newDots: { x: number; y: number }[] = [];
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
newDots.push({
x: col * gridSize,
y: row * gridSize,
});
}
}
setDots(newDots);
const handleResize = () => {
const newRect = container.getBoundingClientRect();
const newCols = Math.ceil(newRect.width / gridSize) + 1;
const newRows = Math.ceil(newRect.height / gridSize) + 1;
const resizedDots: { x: number; y: number }[] = [];
for (let row = 0; row < newRows; row++) {
for (let col = 0; col < newCols; col++) {
resizedDots.push({
x: col * gridSize,
y: row * gridSize,
});
}
}
setDots(resizedDots);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [gridSize]);
const handleMouseMove = (event: React.MouseEvent) => {
const rect = containerRef.current?.getBoundingClientRect();
if (!rect) return;
setMousePos({
x: event.clientX - rect.left,
y: event.clientY - rect.top,
});
};
const handleMouseLeave = () => {
setMousePos({ x: -1000, y: -1000 });
};
const getDisplacement = (dotX: number, dotY: number) => {
const dx = mousePos.x - dotX;
const dy = mousePos.y - dotY;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > influenceRadius || distance === 0) {
return { x: 0, y: 0 };
}
const force = (1 - distance / influenceRadius) * maxDisplacement;
const angle = Math.atan2(dy, dx);
return {
x: -Math.cos(angle) * force,
y: -Math.sin(angle) * force,
};
};
// Respect reduced motion preference
const prefersReducedMotion =
typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
return (
<div
ref={containerRef}
className="relative h-full w-full overflow-hidden"
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
style={{ background: 'linear-gradient(135deg, #111111 0%, #1a1a2e 100%)' }}
>
{dots.map((dot, index) => {
const displacement = prefersReducedMotion ? { x: 0, y: 0 } : getDisplacement(dot.x, dot.y);
const distance = Math.sqrt(
Math.pow(mousePos.x - dot.x, 2) + Math.pow(mousePos.y - dot.y, 2)
);
const scale = distance < influenceRadius ? 1 + (1 - distance / influenceRadius) * 0.5 : 1;
const opacity =
distance < influenceRadius ? 0.8 + (1 - distance / influenceRadius) * 0.2 : 0.3;
return (
<div
key={index}
className="absolute rounded-full transition-transform duration-75"
style={{
width: dotSize,
height: dotSize,
backgroundColor: dotColor,
left: dot.x - dotSize / 2,
top: dot.y - dotSize / 2,
transform: `translate(${displacement.x}px, ${displacement.y}px) scale(${scale})`,
opacity,
}}
/>
);
})}
</div>
);
}
```
## Customization
### Dense Grid
For a more detailed, intricate look:
```tsx
<GridDistortion gridSize={15} dotSize={2} maxDisplacement={15} influenceRadius={100} />
```
### Sparse Grid
For a minimal, spacious feel:
```tsx
<GridDistortion gridSize={50} dotSize={4} maxDisplacement={30} influenceRadius={200} />
```
### Brand Colors
Match your brand palette:
```tsx
<GridDistortion dotColor="#FF3B30" />
```
### Light Background
For light themes, invert the colors in the component's background style:
```tsx
style={{ background: 'linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%)' }}
```
And use a darker dot color like `dotColor="#333333"`.
## Performance Considerations
- **Grid density**: Higher `gridSize` values = fewer dots = better performance
- **Mobile optimization**: Consider increasing `gridSize` to 40-50 on mobile
- **Transition duration**: The `duration-75` class keeps animations snappy without lag
- **Virtual rendering**: For very large grids, consider implementing virtualization
## Accessibility
- Respects `prefers-reduced-motion` - disables displacement animations
- Effect is purely visual decoration
- Ensure any overlaid content has sufficient contrast
## Variations
### Attraction Mode
Change the displacement direction for an "attraction" effect:
```tsx
return {
x: Math.cos(angle) * force, // Remove the negative
y: Math.sin(angle) * force,
};
```
### Color Gradient
Add distance-based color intensity:
```tsx
const intensity =
distance < influenceRadius ? Math.floor(255 * (1 - distance / influenceRadius)) : 0;
const dynamicColor = `rgb(${intensity}, 122, 255)`;
```
### Line Grid
Connect dots with lines for a mesh effect (additional complexity, requires canvas):
```tsx
// Draw lines between adjacent dots
ctx.beginPath();
ctx.moveTo(dots[i].x, dots[i].y);
ctx.lineTo(dots[i + 1].x, dots[i + 1].y);
ctx.stroke();
```