Backgrounds
FreeDot 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.
npx @galactrex/ui add dot-gridUsage
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.
"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
| Prop | Type | Default | Description |
|---|---|---|---|
gap | number | 22 | Distance between dots in px. |
radius | number | 1.1 | Dot radius at rest in px. |
reach | number | 120 | Pointer reach in px; 0 turns it off. |
wave | boolean | false | A slow ripple from the centre. |
opacity | number | 0.45 | Dot strength at rest, 0 to 1. |
color | string | "--gxui-muted" | CSS colour or --gxui-* name. |
className | string | — | 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.