# Retro Grid

The RetroGrid component lays an infinite grid back in 3D and scrolls it toward the viewer, fading out into a horizon. A synthwave backdrop drawn entirely in CSS.

- **Category:** Background
- **Docs:** https://uibeats.com/docs/background/retro-grid
- **Registry:** https://uibeats.com/r/retro-grid.json
- **License:** MIT
- **npm dependencies:** `motion`

## Installation

```bash
npx shadcn@latest add https://uibeats.com/r/retro-grid.json
```

Or install the dependencies and copy the source below:

```bash
npm install motion
```

## When to use

Behind a hero or a section header that needs depth without downloading a background image. Keep the colour faint or it competes with the copy on top.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `cellSize` | `number` | `60` | Size of one grid cell, in pixels |
| `duration` | `number` | `2.2` | Seconds for the grid to travel one full cell (lower is faster) |
| `angle` | `number` | `65` | How far the plane is laid back, in degrees |
| `lineWidth` | `number` | `1` | Thickness of the grid lines, in pixels |
| `height` | `number` | `0.7` | Fraction of the frame the grid fills, measured up from the bottom |
| `className` | `string` | `''` | Additional CSS classes. Set the line colour here, e.g. text-brand/40 |

## Example usage

```tsx
"use client";

import { RetroGrid } from "@/components/demo/background/retro-grid";

const RetroGridUsage = () => {
  return (
    <div className="relative size-full overflow-hidden bg-background">
      <RetroGrid className="text-brand/50" cellSize={54} duration={2.4} />

      <div className="relative z-10 flex size-full flex-col items-center justify-center px-6 text-center">
        <h2 className="bg-gradient-to-b from-foreground to-foreground/40 bg-clip-text text-4xl font-bold tracking-tighter text-transparent">
          Ship it
        </h2>
        <p className="mt-2 max-w-xs text-sm text-muted-foreground">
          An endless grid running to the horizon, drawn in whatever colour you
          set on it.
        </p>
      </div>
    </div>
  );
};

export default RetroGridUsage;
```

## Source

Save as `components/ui/retro-grid.tsx`.

```tsx
"use client";

import { motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

interface RetroGridProps {
  /** Size of one grid cell, in pixels. */
  cellSize?: number;
  /** Seconds for the grid to travel one full cell. Lower is faster. */
  duration?: number;
  /** How far the plane is laid back, in degrees. 90 would be edge on. */
  angle?: number;
  /** Thickness of the grid lines, in pixels. */
  lineWidth?: number;
  /** Fraction of the frame the grid fills, measured up from the bottom. */
  height?: number;
  className?: string;
}

/**
 * An infinite grid receding to a horizon and scrolling toward the viewer.
 *
 * Lines are drawn with `currentColor`, so the grid takes the text colour of
 * whatever wraps it and follows the theme without a single JS read. Depth is a
 * CSS `perspective` plus one `rotateX`, and motion is a single transform on an
 * oversized tile. The loop is seamless because it travels exactly one cell
 * before restarting, which is indistinguishable from travelling forever.
 */
export function RetroGrid({
  cellSize = 60,
  duration = 2.2,
  angle = 65,
  lineWidth = 1,
  height = 0.7,
  className = "",
}: RetroGridProps) {
  const prefersReducedMotion = useReducedMotion();

  // Fades the grid out as it approaches the horizon. A mask rather than a
  // gradient overlay, so it works over whatever the parent paints behind it.
  const horizonFade =
    "linear-gradient(to top, rgba(0,0,0,1) 0%, rgba(0,0,0,0.35) 55%, transparent 88%)";

  return (
    <div
      aria-hidden="true"
      className={cn(
        "pointer-events-none absolute inset-0 overflow-hidden",
        className,
      )}
      style={{
        perspective: "200px",
        maskImage: horizonFade,
        WebkitMaskImage: horizonFade,
      }}
    >
      <div
        className="absolute inset-x-0 bottom-0"
        style={{
          height: `${height * 100}%`,
          transform: `rotateX(${angle}deg)`,
          transformOrigin: "bottom",
        }}
      >
        {/*
         * The tile is far wider and taller than the frame on purpose: laying it
         * back in 3D throws its far edge up to the horizon and its sides out
         * past the corners, so anything frame-sized would show its own edges
         * as the grid moves.
         */}
        <motion.div
          className="absolute -inset-x-[200%] bottom-0 h-[300%]"
          style={{
            backgroundImage: `linear-gradient(to right, currentColor ${lineWidth}px, transparent 0), linear-gradient(to bottom, currentColor ${lineWidth}px, transparent 0)`,
            backgroundSize: `${cellSize}px ${cellSize}px`,
          }}
          animate={prefersReducedMotion ? undefined : { y: [0, cellSize] }}
          transition={{ duration, repeat: Infinity, ease: "linear" }}
        />
      </div>
    </div>
  );
}

export default RetroGrid;
```

## Credits

Written by [Claude Code](https://claude.com/claude-code).
