Dot Grid
A background grid of dots that pushes away from the cursor and springs back.
Installation
pnpm dlx shadcn@latest add @uigrove/dot-gridDependencies: none. Installs @uigrove/dot-canvas alongside.
First time? Add the registry to components.json
{
"registries": {
"@uigrove": "https://uigrove.me/r/{name}.json"
}
}Usage
import { DotGrid } from '@/components/uigrove/dot-grid'
<DotGrid className="absolute inset-0" />Props
| Name | Type | Default | Description |
|---|---|---|---|
| dotSize | number | 2 | Dot size |
| gap | number | 24 | Gap |
| color | string | '#ffffff' | Color |
| opacity | number | 0.35 | Opacity |
| shape | 'circle' | 'square' | 'circle' | Shape |
| radius | number | 140 | Cursor radius |
| strength | number | 28 | Push strength |
| stiffness | number | 160 | Spring stiffness |
| damping | number | 16 | Damping |
| ripple | boolean | true | Click ripple |
| rippleStrength | number | 18 | Ripple strength |
| interactive | boolean | true | Interactive |
| className | string | — | Classes for the root element. Use it for sizing and position. |
Code
'use client'
import { useEffect, useRef, type CSSProperties } from 'react'
import { useDotCanvas } from '@/hooks/use-dot-canvas'
import { DotGridEngine, type DotGridOptions } from './dot-grid.engine'
export interface DotGridProps extends Partial<DotGridOptions> {
className?: string
style?: CSSProperties
}
export const dotGridDefaults: DotGridOptions = {
dotSize: 2,
gap: 24,
color: '#ffffff',
opacity: 0.35,
shape: 'circle',
radius: 140,
strength: 28,
stiffness: 160,
damping: 16,
ripple: true,
rippleStrength: 18,
interactive: true,
}
export function DotGrid({
dotSize = dotGridDefaults.dotSize,
gap = dotGridDefaults.gap,
color = dotGridDefaults.color,
opacity = dotGridDefaults.opacity,
shape = dotGridDefaults.shape,
radius = dotGridDefaults.radius,
strength = dotGridDefaults.strength,
stiffness = dotGridDefaults.stiffness,
damping = dotGridDefaults.damping,
ripple = dotGridDefaults.ripple,
rippleStrength = dotGridDefaults.rippleStrength,
interactive = dotGridDefaults.interactive,
className,
style,
}: DotGridProps) {
const engineRef = useRef<DotGridEngine | null>(null)
if (!engineRef.current) engineRef.current = new DotGridEngine(dotGridDefaults)
const engine = engineRef.current
const { containerRef, canvasRef, wake } = useDotCanvas(engine, { pointer: interactive })
useEffect(() => {
engine.setOptions({
dotSize,
gap,
color,
opacity,
shape,
radius,
strength,
stiffness,
damping,
ripple,
rippleStrength,
interactive,
})
wake()
}, [
engine,
wake,
dotSize,
gap,
color,
opacity,
shape,
radius,
strength,
stiffness,
damping,
ripple,
rippleStrength,
interactive,
])
useEffect(() => {
if (!ripple || !interactive) return
const container = containerRef.current
if (!container) return
const onDown = (e: PointerEvent) => {
const rect = container.getBoundingClientRect()
engine.addRipple(e.clientX - rect.left, e.clientY - rect.top)
wake()
}
window.addEventListener('pointerdown', onDown, { passive: true })
return () => window.removeEventListener('pointerdown', onDown)
}, [engine, wake, ripple, interactive, containerRef])
return (
<div
ref={containerRef}
className={['uig-dot-grid', 'pointer-events-none overflow-hidden', className]
.filter(Boolean)
.join(' ')}
style={style}
aria-hidden="true"
>
<canvas ref={canvasRef} className="block h-full w-full" />
</div>
)
}