Account

Backgrounds

Free

Dot Grid

Quiet texture for any section. Dots near the pointer swell and brighten, then settle; an optional ripple keeps it alive on touch screens.

Install

No key needed.

Terminal
npx @galactrex/ui add dot-grid

Usage

Example
import { DotGrid } from "@/components/galactrex/dot-grid";

<section className="relative overflow-hidden">
  <DotGrid />
  <h1 className="relative">Hello</h1>
</section>

Source

Installs to components/galactrex/dot-grid.tsx, with the shared motion primitives beside it.

components/galactrex/dot-grid.tsx
"use client";
import { useCallback, useRef } from "react";
import { cx } from "@/components/galactrex/motion/cx";
import { useCanvas, useParentPointer, type CanvasFrame } from "@/components/galactrex/motion/use-canvas";
import { useElementSize } from "@/components/galactrex/motion/use-element-size";
import { useFinePointer } from "@/components/galactrex/motion/use-fine-pointer";
import { useReducedMotion } from "@/components/galactrex/motion/use-reduced-motion";
import { useVisible } from "@/components/galactrex/motion/use-visible";

export type DotGridProps = {
  /** distance between dots in px */
  gap?: number;
  /** dot radius in px at rest */
  radius?: number;
  /** dots within this many px of the pointer swell and brighten; 0 turns it off */
  reach?: number;
  /** a slow ripple that travels out from the centre */
  wave?: boolean;
  /** strength of a dot at rest, 0 to 1 */
  opacity?: number;
  /** any CSS colour or a --gxui-* name; defaults to the theme muted ink */
  color?: string;
  className?: string;
};

/** A quiet matrix of dots that wakes up under the pointer: nearby dots swell and brighten, then settle when it
    leaves. An optional ripple keeps it alive on touch screens. Place it inside a relative parent, behind content.
    Pauses offscreen; with reduced motion the dots hold still. */
export function DotGrid({ gap = 22, radius = 1.1, reach = 120, wave = false, opacity = 0.45, color = "--gxui-muted", className }: DotGridProps) {
  const host = useRef<HTMLDivElement>(null);
  const size = useElementSize(host);
  const fine = useFinePointer();
  const reduced = useReducedMotion();
  const visible = useVisible(host);
  const follow = fine && !reduced && reach > 0;
  const point = useParentPointer(host, follow);

  const setup = useCallback(({ ctx, width, height, color: read, pointer }: CanvasFrame) => {
    const step = Math.max(6, gap);
    const cols = Math.ceil(width / step) + 1, rows = Math.ceil(height / step) + 1;
    const offX = (width - (cols - 1) * step) / 2, offY = (height - (rows - 1) * step) / 2;
    // each dot eases toward its target, so the swell trails the pointer instead of snapping
    const lift = new Float32Array(cols * rows);
    const base = Math.min(1, Math.max(0, opacity));
    const cx0 = width / 2, cy0 = height / 2;
    return (t: number, dt: number) => {
      const p = pointer();
      ctx.clearRect(0, 0, width, height);
      ctx.fillStyle = read(color);
      const ease = dt ? Math.min(1, dt * 10) : 1;
      for (let j = 0; j < rows; j++) {
        for (let i = 0; i < cols; i++) {
          const x = offX + i * step, y = offY + j * step, k = j * cols + i;
          let target = 0;
          if (p && reach > 0) {
            const d = Math.hypot(x - p.x, y - p.y);
            if (d < reach) target = (1 - d / reach) ** 2;
          }
          if (wave && dt) {
            const d = Math.hypot(x - cx0, y - cy0);
            target = Math.max(target, 0.5 * Math.max(0, Math.sin(d / 38 - t * 2.2)) ** 6);
          }
          lift[k] += (target - lift[k]) * ease;
          const l = lift[k];
          ctx.globalAlpha = base + (1 - base) * l;
          ctx.beginPath();
          ctx.arc(x, y, radius * (1 + l * 1.6), 0, Math.PI * 2);
          ctx.fill();
        }
      }
      ctx.globalAlpha = 1;
    };
  }, [gap, radius, reach, wave, opacity, color]);

  const canvas = useCanvas(size, setup, visible && !reduced && (follow || wave), point);

  return (
    <div ref={host} aria-hidden className={cx("pointer-events-none absolute inset-0 overflow-hidden", className)}>
      <canvas ref={canvas} className="size-full" />
    </div>
  );
}

Props

PropTypeDefaultDescription
gapnumber22Distance between dots in px.
radiusnumber1.1Dot radius at rest in px.
reachnumber120Pointer reach in px; 0 turns it off.
wavebooleanfalseA slow ripple from the centre.
opacitynumber0.45Dot strength at rest, 0 to 1.
colorstring"--gxui-muted"CSS colour or --gxui-* name.
classNamestring—Extra classes for the outer element.

Variants

  • Pointer. Swells under a mouse or pen.
  • Wave. `wave` ripples outward without a pointer.

Accessibility

  • Decorative only and aria-hidden; never takes pointer events (it listens on the parent).

Motion and reduced motion

  • Dots ease toward their target, so the swell trails the pointer.
  • Idle with no pointer and no wave.
  • Reduced motion: still dots.