Dot Grid
Installation
npx shadcn@latest add https://uibrik.com/r/dot-grid.jsonAdd the cn helper
Imports
cnfrom@/lib/utils, whichshadcn initcreates. Add it by hand without shadcn.Copy the source into your project
components/uibrik/dot-grid/dot-grid.tsx"use client"; import { hexToOklab } from "../_shader-background/color"; import { ShaderSurface, type ShaderBackgroundProps } from "../_shader-background/ShaderSurface"; // A grid of dots with light moving through it: a slow wave of colour, the odd dot twinkling, rings // that spread from a click, and a soft light that glides after the pointer. That light lerps behind // the cursor with a lazier tail (a gentle comet while moving, a circle at rest), and the grid bulges // under it like a loupe. Lit dots grow a little and // cast a faint halo, and a pale wash of the palette drifts underneath so the grid never feels bare. // Dots are drawn per pixel with a one-pixel soft edge, so they stay crisp at any size. const SHADER = /* glsl */ ` uniform float u_spacing; // distance between dots, css px uniform float u_size; // dot diameter, css px uniform float u_radius; // reach of the pointer light, css px uniform float u_intensity; // strength of the drifting wave uniform float u_twinkle; // how often single dots sparkle uniform float u_glow; // halo around lit dots uniform float u_ambient; // colour wash under the grid uniform float u_fade; // fade toward the edges, 0..1 uniform float u_shape; // 0 circle, 1 square, 2 diamond uniform float u_stagger; // 0 grid, 1 every other row shifted half a step uniform vec3 u_dotColor; // resting dot colour, OKLab uniform float u_hasDotColor; // 0: resting dots follow the theme uniform float u_magnify; // loupe strength under the pointer light uniform float u_trailAmt; // how much the light stretches toward its tail float shapeDist(vec2 d) { if (u_shape < 0.5) return length(d); if (u_shape < 1.5) return max(abs(d.x), abs(d.y)) * 1.1; return (abs(d.x) + abs(d.y)) * 0.8; } vec3 shade(vec2 frag) { // loupe: sample the grid closer to the light, so dots there look larger and further apart float R = u_radius * u_dpr; vec2 toP = frag - u_pointer; float lens = u_magnify * 0.38 * exp(-dot(toP, toP) / (R * R * 0.55)) * u_pointerAmt; vec2 screen = frag; frag = u_pointer + toP * (1.0 - lens); float sp = u_spacing * u_dpr; float row = floor(frag.y / sp); float shift = u_stagger * mod(row, 2.0) * 0.5 * sp; vec2 cell = vec2(floor((frag.x - shift) / sp), row); vec2 center = vec2((cell.x + 0.5) * sp + shift, (row + 0.5) * sp); vec2 q = center / u_res.y; vec2 uv = center / u_res; float t = u_time * 0.12; float wave = smoothstep(0.35, 0.95, snoise(vec3(q * 1.3, t)) * 0.5 + 0.5) * u_intensity; float h = hash12(cell + 3.7); float twinkle = pow(0.5 + 0.5 * sin(u_time * (0.5 + h * 0.8) + h * 40.0), 14.0) * u_twinkle; float pd = pointerDist(center, u_trailAmt) / R; float spot = exp(-pd * pd * 2.5) * u_pointerAmt; float light = clamp(wave + twinkle + spot + ripples(center), 0.0, 1.0); vec3 lit = paletteLoop(q.x * 0.2 + q.y * 0.1 + t * 0.4); vec3 rest = u_hasDotColor > 0.5 ? u_dotColor : vec3(mix(0.84, 0.34, u_dark), 0.0, 0.0); vec3 dotColor = mix(rest, lit, light); // a pale wash of the palette drifting under the grid vec2 fuv = screen / u_res.y; float wash = smoothstep(-0.3, 0.9, snoise(vec3(fuv * 0.8, t * 0.6))); vec3 base = mix(u_bg, paletteLoop(t * 0.2 + fuv.x * 0.3), wash * u_ambient * mix(0.1, 0.16, u_dark)); // a faint halo around lit dots float d = length(frag - center); base = mix(base, lit, exp(-d * d / pow(sp * 0.45, 2.0)) * light * u_glow * mix(0.22, 0.4, u_dark)); float edge = smoothstep(0.0, 0.2, min(min(uv.x, 1.0 - uv.x), min(uv.y, 1.0 - uv.y))); float r = u_size * 0.5 * u_dpr * (1.0 + 0.45 * light); float mask = 1.0 - smoothstep(r - 0.75, r + 0.75, shapeDist(frag - center)); mask *= mix(mix(1.0, edge, u_fade), 1.0, light); return oklabToLinear(mix(base, dotColor, mask)); } `; const COLORS = ["#2fb4d2", "#3a9ef2", "#5a82ea", "#34c1b0"]; const SHAPES = { circle: 0, square: 1, diamond: 2 } as const; export type DotGridProps = ShaderBackgroundProps & { /** Distance between dots, in px. */ spacing?: number; /** Dot diameter, in px. */ size?: number; shape?: keyof typeof SHAPES; /** "stagger" shifts every other row by half a step. */ pattern?: "grid" | "stagger"; /** Resting dot colour; empty follows the theme. */ dotColor?: string; /** Base colour behind the dots; empty follows the page. */ background?: string; /** Strength of the drifting colour wave; 0 leaves only the pointer light. */ intensity?: number; /** How often single dots sparkle. */ twinkle?: number; /** Soft halo around lit dots. */ glow?: number; /** Pale wash of colour drifting under the grid. */ ambient?: number; /** Fade toward the edges, so the grid has no hard border. */ fade?: number; /** How far the pointer light reaches, in px. */ radius?: number; /** A click sends a ring of light across the grid. */ ripples?: boolean; /** How lazily the light follows the pointer, 0 (locked) to 1 (drifts after it). */ follow?: number; /** How much the light stretches into a tail while the pointer moves. */ trail?: number; /** Loupe strength: how much the dots under the light are enlarged. */ magnify?: number; }; export function DotGrid({ colors = COLORS, speed = 1, animate = true, interactive = true, grain = 0, seed = 0, spacing = 20, size = 2.5, shape = "circle", pattern = "grid", dotColor = "", background = "", intensity = 0.7, twinkle = 0.5, glow = 0.6, ambient = 0.5, fade = 1, radius = 160, ripples = true, follow = 0.6, trail = 0.6, magnify = 0.5, ...rest }: DotGridProps) { const dot = dotColor ? hexToOklab(dotColor) : [0, 0, 0]; return ( <ShaderSurface shader={SHADER} uniforms={{ u_spacing: spacing, u_size: size, u_radius: radius, u_intensity: intensity, u_twinkle: twinkle, u_glow: glow, u_ambient: ambient, u_fade: fade, u_shape: SHAPES[shape] ?? 0, u_stagger: pattern === "stagger" ? 1 : 0, u_hasDotColor: dotColor ? 1 : 0, u_magnify: magnify, u_trailAmt: trail, }} vectors={{ u_dotColor: dot }} colors={colors} speed={speed} animate={animate} interactive={interactive} grain={grain} seed={seed} background={background} ripples={ripples} follow={follow} maxDpr={2} fallback={(dark, base) => { const dots = dotColor || (dark ? "#3a3a3a" : "#d4d4d4"); return `radial-gradient(circle at 50% 50%, ${colors[0]}22, transparent 60%), radial-gradient(${dots} ${size / 2}px, transparent ${size / 2 + 0.5}px) 0 0 / ${spacing}px ${spacing}px, ${base}`; }} {...rest} /> ); }
Adds the files to
components/uibrik/dot-grid. With the @uibrik namespace set up: npx shadcn@latest add @uibrik/dot-grid.Props
DotGrid- colors
- Type string[]
- ["#2fb4d2","#3a9ef2","#5a82ea","#34c1b0"]
- Colours the dots light up in, 2 to 6.
- dotColor
- Type string
- ""
- Resting dot colour; empty follows the theme.
- background
- Type string
- ""
- Base colour behind the dots; empty follows the page.
- shape
- Type "circle" | "square" | "diamond"
- "circle"
- Dot shape.
- pattern
- Type "grid" | "stagger"
- "grid"
- "stagger" shifts every other row by half a step.
- spacing
- Type number
- 20
- Distance between dots.
- size
- Type number
- 2.5
- Dot diameter.
- intensity
- Type number
- 0.7
- Strength of the drifting colour wave; 0 leaves only the pointer light.
- twinkle
- Type number
- 0.5
- How often single dots sparkle.
- glow
- Type number
- 0.6
- Soft halo around lit dots.
- ambient
- Type number
- 0.5
- Pale wash of colour drifting under the grid.
- fade
- Type number
- 1
- Fade toward the edges, so the grid has no hard border.
- speed
- Type number
- 1
- Animation speed; 1 is the designed pace.
- grain
- Type number
- 0
- Film grain strength.
- seed
- Type number
- 0
- Shifts the composition, so two backgrounds with the same settings still differ.
- animate
- Type boolean
- true
- Move on its own. Off draws one still frame.
- interactive
- Type boolean
- true
- React to the pointer.
- radius
- Type number
- 160
- How far the pointer light reaches.
- follow
- Type number
- 0.6
- How lazily the light follows the pointer, 0 (locked) to 1 (drifts after it).
- trail
- Type number
- 0.6
- How much the light stretches into a tail while the pointer moves.
- magnify
- Type number
- 0.5
- Loupe strength: how much the dots under the light are enlarged.
- ripples
- Type boolean
- true
- A click sends a ring of light across the grid.
- children
- Type ReactNode
- –
- Content shown on top of the background.
- className
- Type string
- –
- Classes for the box; it fills whatever size you give it.