pnpm dlx shadcn@latest add @uigrove/dot-grid

Dot Grid

A background grid of dots that pushes away from the cursor and springs back.

Installation
pnpm dlx shadcn@latest add @uigrove/dot-grid

Dependencies: 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
NameTypeDefaultDescription
dotSizenumber2Dot size
gapnumber24Gap
colorstring'#ffffff'Color
opacitynumber0.35Opacity
shape'circle' | 'square''circle'Shape
radiusnumber140Cursor radius
strengthnumber28Push strength
stiffnessnumber160Spring stiffness
dampingnumber16Damping
ripplebooleantrueClick ripple
rippleStrengthnumber18Ripple strength
interactivebooleantrueInteractive
classNamestring—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>
  )
}