uniquel
componentbackground-gridsource: ok · hit
Background Grid
A static 3D background grid - a glowing wireframe perspective grid that recedes into the dark, with depth from atmospheric falloff and light pooling at each intersection.
npx shadcn add "https://uniquel.io/r/background-grid.json"free
Where it goes
stats
Visual style
3ddark-futuristic
Interaction
animated-counter
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Files1 · source cached 10/14/2026, 7:41:28 PM
components/backgrounds/BackgroundGrid.tsx
'use client';
import React, { useEffect, useRef, type ReactNode } from 'react';
// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------
//
// children Content layered above the grid (headline, CTA, etc.)
// className Styles for the content wrapper
// containerClassName Styles for the outer shell (size, layout)
// theme "dark" (default) | "light"
// backgroundFill Overrides the theme's background color (hex)
// lineColor Overrides the theme's grid line color (hex)
// cells Grid density (lines per axis)
//
type BackgroundGridProps = {
children?: ReactNode;
className?: string;
containerClassName?: string;
theme?: 'dark' | 'light';
backgroundFill?: string;
lineColor?: string;
cells?: number;
};
// ---------------------------------------------------------------------------
// #rrggbb -> "r, g, b" so alpha can vary per segment
// ---------------------------------------------------------------------------
function toRgb(hex: string) {
const h = hex.replace('#', '');
const full =
h.length === 3
? h
.split('')
.map((c) => c + c)
.join('')
: h;
const n = parseInt(full, 16);
return `${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}`;
}
// ---------------------------------------------------------------------------
// Per-theme defaults.
// ---------------------------------------------------------------------------
const THEMES = {
dark: {
background: '#02040c',
line: '#2563eb',
},
light: {
background: '#eef2ff',
line: '#1d4ed8',
},
} as const;
export default function BackgroundGrid({
children,
className = '',
containerClassName = '',
theme = 'dark',
backgroundFill,
lineColor = '#7eeab4',
cells = 14,
}: BackgroundGridProps) {
const tokens = THEMES[theme] ?? THEMES.dark;
const bg = backgroundFill ?? tokens.background;
const line = lineColor ?? tokens.line;
const containerRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
const container = containerRef.current;
if (!canvas || !container) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const rgb = toRgb(line);
let width = 0;
let height = 0;
// -------------------------------------------------------------------------
// Warp a flat (u, v) grid — both axes 0..1 — into a full-bleed wavy plane.
// A radial pincushion pull toward an off-frame vanishing point bows every
// line, while depth (distance from that point) drives fade + thickness.
// Because u and v both span 0..1 edge to edge, the canvas is fully covered
// — no fan, no dead wedge.
// -------------------------------------------------------------------------
const vpx = 1.05; // vanishing point sits just outside the top-right corner
const vpy = -0.08;
const reach = 1.7; // how far the pull's influence extends across the plane
const pad = 0.38;
const uMin = -pad;
const vMin = -pad;
const span = 1 + pad * 2;
const lineCount = Math.round(cells * span);
const project = (u: number, v: number) => {
const dx = u - vpx;
const dy = v - vpy;
const dist = Math.hypot(dx, dy);
const t = Math.max(0, Math.min(1, dist / reach));
// Compress points toward the vanishing point — strongest close in,
// tapering to none at the far edge. This is what bows straight lines
// into curves and packs them tight near the corner, like the reference.
const compress = 1 - t;
const wx = vpx + dx * (t + compress * compress * 1.15);
const wy = vpy + dy * (t + compress * compress * 1.15);
const depth = compress;
return { x: wx * width, y: wy * height, depth };
};
const draw = () => {
ctx.fillStyle = bg;
ctx.fillRect(0, 0, width, height);
ctx.lineCap = 'round';
const steps = 24; // samples per line, enough to render the curve smoothly
// Lines of constant u (verticals before the warp)
for (let i = 0; i <= lineCount; i++) {
const u = uMin + (i / lineCount) * span;
ctx.beginPath();
for (let s = 0; s <= steps; s++) {
const p = project(u, vMin + (s / steps) * span);
if (s === 0) ctx.moveTo(p.x, p.y);
else ctx.lineTo(p.x, p.y);
}
const mid = project(u, 0.5);
ctx.strokeStyle = `rgba(${rgb}, ${0.3 + mid.depth * 0.6})`;
ctx.lineWidth = 0.8 + mid.depth * 2.2;
ctx.stroke();
}
// Lines of constant v (horizontals before the warp)
for (let j = 0; j <= lineCount; j++) {
const v = vMin + (j / lineCount) * span;
ctx.beginPath();
for (let s = 0; s <= steps; s++) {
const p = project(uMin + (s / steps) * span, v);
if (s === 0) ctx.moveTo(p.x, p.y);
else ctx.lineTo(p.x, p.y);
}
const mid = project(0.5, v);
ctx.strokeStyle = `rgba(${rgb}, ${0.3 + mid.depth * 0.6})`;
ctx.lineWidth = 0.8 + mid.depth * 2.2;
ctx.stroke();
}
};
const resize = () => {
const rect = container.getBoundingClientRect();
const dpr = Math.min(window.devicePixelRatio || 1, 2);
width = Math.max(1, Math.floor(rect.width));
height = Math.max(1, Math.floor(rect.height));
canvas.width = Math.floor(width * dpr);
canvas.height = Math.floor(height * dpr);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
draw();
};
resize();
const observer = new ResizeObserver(resize);
observer.observe(container);
return () => observer.disconnect();
}, [bg, line, cells]);
return (
<div
ref={containerRef}
className={[
'relative flex min-h-[420px] w-full flex-col items-center justify-center overflow-hidden',
containerClassName,
]
.filter(Boolean)
.join(' ')}
style={{ backgroundColor: bg }}
>
<canvas
ref={canvasRef}
className='absolute inset-0 z-0 h-full w-full'
aria-hidden
/>
<div
className={['relative z-10 px-6 text-center', className]
.filter(Boolean)
.join(' ')}
>
{children}
</div>
</div>
);
}