shadimage
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>
  );
}

More from uniquel

Browse all components in this library →