# Retro Grid

> Synthwave perspective grid that scrolls toward the horizon.

Category: Backgrounds. Docs: https://neonkit.heyaadi.com/components/retro-grid

## Installation

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

With the `@neonkit` namespace configured: `npx shadcn@latest add @neonkit/retro-grid`.

npm dependencies: none. Also needs `lib/utils.ts` (`cn`) and `lib/neon.ts` (`neonVars`), which the CLI adds.

## Usage

```tsx
import { RetroGrid } from "@/components/ui/retro-grid"

<section className="relative overflow-hidden">
  <RetroGrid color="magenta" />
  <h1 className="relative">Hello</h1>
</section>
```

## Examples

### Hero background

The floor is one long plane in real perspective, so rows compress toward the horizon and the nearest squares draw at twice cellSize.

```tsx
import { RetroGrid } from "@/components/ui/retro-grid";

export default function RetroGridDemo() {
  return (
    <div className="relative flex h-80 w-full flex-col items-center overflow-hidden rounded-xl bg-[#05060a] px-6 pt-12 text-center sm:h-96 sm:pt-16">
      <RetroGrid />
      <h3 className="relative text-4xl font-semibold tracking-[-0.03em] text-white/95 sm:text-5xl">Nightcall, side B</h3>
      <p className="relative mt-3 max-w-sm text-[15px] leading-relaxed text-white/60">
        Nine tracks at 118 BPM. Out Friday on tape and everywhere else.
      </p>
    </div>
  );
}
```

### Angle, cell size and speed

```tsx
import { RetroGrid } from "@/components/ui/retro-grid";

export default function RetroGridSettings() {
  return (
    <div className="grid w-full gap-3 sm:grid-cols-2">
      <div className="relative h-56 overflow-hidden rounded-xl border border-white/10 bg-[#05060a]">
        <RetroGrid angle={60} cellSize={40} speed={0.8} />
        <p className="absolute left-4 top-4 font-mono text-xs text-white/50">angle 60 · cell 40 · speed 0.8</p>
      </div>
      <div className="relative h-56 overflow-hidden rounded-xl border border-white/10 bg-[#05060a]">
        <RetroGrid angle={78} cellSize={72} speed={0} horizon={false} opacity={0.45} />
        <p className="absolute left-4 top-4 font-mono text-xs text-white/50">angle 78 · cell 72 · still, no horizon</p>
      </div>
    </div>
  );
}
```

## API reference

### RetroGrid

Fills its nearest positioned parent. The horizon sits halfway down.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `color` | `NeonColor` | `inherited` | Line color. |
| `angle` | `number` | `70` | Floor tilt in degrees, 20 to 88. Higher is a flatter, longer floor. |
| `cellSize` | `number` | `56` | Grid square size in pixels on the floor; the row nearest the viewer draws at twice this. |
| `speed` | `number` | `1.2` | Seconds to travel one cell. 0 stops it. |
| `opacity` | `number` | `0.55` | Line opacity. |
| `horizon` | `boolean` | `true` | Glowing horizon line. |

## Accessibility

- Decorative: rendered with aria-hidden and no pointer events. Stops moving under reduced motion.
- The horizon is computed from the angle with container query units, so it stays on the glowing line at any height. Needs container units (every browser since 2023).

## Source

`components/ui/retro-grid.tsx`

```tsx
"use client";

import * as React from "react";

import { cn } from "@/lib/utils";
import { neonVars, type NeonColor } from "@/lib/neon";

const css = `
@keyframes nk-retro-grid { from { transform: translateY(calc(var(--cell) * -1)); } to { transform: translateY(0); } }
.nk-retro-grid { animation: nk-retro-grid var(--speed) linear infinite; }
@media (prefers-reduced-motion: reduce) { .nk-retro-grid { animation: none; } }
`;

export interface RetroGridProps
  extends Omit<React.HTMLAttributes<HTMLDivElement>, "color"> {
  /** Line color. Inherits `--neon-accent` when omitted. */
  color?: NeonColor;
  /** Tilt of the floor in degrees. */
  angle?: number;
  /** Size of one grid square in pixels. */
  cellSize?: number;
  /** Seconds for the floor to travel one cell. 0 stops it. */
  speed?: number;
  /** Line opacity, 0 to 1. */
  opacity?: number;
  /** Glowing line where the floor meets the sky. */
  horizon?: boolean;
}

/**
 * A background layer. Place it inside a `relative` parent; it fills it.
 */
const RetroGrid = React.forwardRef<HTMLDivElement, RetroGridProps>(
  (
    {
      className,
      style,
      color,
      angle = 70,
      cellSize = 56,
      speed = 1.2,
      opacity = 0.55,
      horizon = true,
      ...props
    },
    ref,
  ) => {
    const tilt = Math.min(88, Math.max(20, angle));
    const tan = Math.tan((tilt * Math.PI) / 180).toFixed(4);
    // How far the plane runs, in container heights × 100. A row at distance s draws at s·sin / (tan/2 + s·sin) of
    // the way to the horizon, so 4.5 / cos(angle) reaches 90%, where the mask has already faded it out. Anything
    // longer is a bigger layer to repaint for nothing: the old fixed 3000 made pages stutter whenever something
    // else on them changed every frame.
    const length = Math.min(3000, Math.max(1500, Math.ceil(450 / Math.cos((tilt * Math.PI) / 180))));
    const vars = {
      "--cell": `${cellSize}px`,
      "--speed": `${speed}s`,
      "--line": `${opacity}`,
      ...style,
    } as React.CSSProperties;

    return (
      <div
        ref={ref}
        aria-hidden
        style={neonVars(color, vars)}
        className={cn(
          "[--neon:var(--neon-accent,34_211_238)]",
          "pointer-events-none absolute inset-0 overflow-hidden",
          className,
        )}
        {...props}
      >
        <style>{css}</style>
        {/* Sky glow: the horizon's light falling off upward */}
        {horizon && (
          <div className="absolute inset-x-0 bottom-1/2 h-1/2 bg-[radial-gradient(ellipse_60%_100%_at_50%_100%,rgb(var(--neon)/CALC(0.16*var(--neon-glow,1))),rgb(var(--neon)/CALC(0.05*var(--neon-glow,1)))_45%,transparent_75%)]" />
        )}
        {/* The floor is a long plane tipped away from the viewer, so its rows really recede. A plane's vanishing
            line sits perspective × cot(angle) above the perspective origin, so with the origin on the floor's
            bottom edge and perspective = floor height × tan(angle), the horizon lands on the glowing line at
            any size. Container units carry the floor height into CSS. */}
        <div className="absolute inset-x-0 bottom-0 top-1/2 [container-type:size] [mask-image:linear-gradient(to_bottom,transparent_6%,rgb(0_0_0/0.4)_30%,#000_66%)]">
          <div
            className="absolute inset-0 [perspective-origin:50%_100%]"
            style={{ perspective: `calc(100cqh * ${tan})` }}
          >
            <div
              className="absolute inset-x-[-120%] bottom-0 origin-bottom overflow-hidden"
              // Pulled toward the viewer by half the perspective, so the nearest squares draw at twice cellSize.
              // The bottom edge sits on the perspective origin, so it stays on the floor's bottom edge.
              style={{ height: `${length}cqh`, transform: `translateZ(calc(50cqh * ${tan})) rotateX(${tilt}deg)` }}
            >
            {/* Each line is a hot core with a soft falloff either side, painted into the tile */}
            <div
              className={cn(
                "absolute inset-x-0 top-0 h-[calc(100%+var(--cell))] [background-position:50%_0] [background-size:var(--cell)_var(--cell)]",
                "[background-image:linear-gradient(to_right,color-mix(in_srgb,rgb(var(--neon)/var(--line))_70%,rgb(255_255_255/var(--line)))_1px,transparent_0),linear-gradient(to_bottom,color-mix(in_srgb,rgb(var(--neon)/var(--line))_70%,rgb(255_255_255/var(--line)))_1px,transparent_0),linear-gradient(to_right,rgb(var(--neon)/calc(var(--line)*0.35)),transparent_4px,transparent_calc(100%_-_4px),rgb(var(--neon)/calc(var(--line)*0.35))),linear-gradient(to_bottom,rgb(var(--neon)/calc(var(--line)*0.35)),transparent_4px,transparent_calc(100%_-_4px),rgb(var(--neon)/calc(var(--line)*0.35)))]",
                speed > 0 && "nk-retro-grid",
              )}
            />
            </div>
          </div>
          {/* Haze on the floor where it meets the horizon */}
          <div className="absolute inset-x-0 top-0 h-1/3 bg-[linear-gradient(to_bottom,rgb(var(--neon)/0.1),transparent)]" />
        </div>
        {horizon && (
          <div className="absolute inset-x-0 top-1/2 h-px -translate-y-px bg-[linear-gradient(to_right,transparent,color-mix(in_srgb,rgb(var(--neon))_60%,white)_20%,color-mix(in_srgb,rgb(var(--neon))_60%,white)_80%,transparent)] shadow-[0_0_4px_rgb(var(--neon)/var(--neon-glow,1)),0_0_14px_1px_rgb(var(--neon)/CALC(0.55*var(--neon-glow,1))),0_0_60px_6px_rgb(var(--neon)/CALC(0.3*var(--neon-glow,1)))]" />
        )}
      </div>
    );
  },
);
RetroGrid.displayName = "RetroGrid";

export { RetroGrid };
```
