# Retro Grid (Moonarc)

A perspective grid floor sliding toward the viewer. Two repeating gradients on one plane rotated in 3D; the motion is a translate of exactly one cell on the compositor, so the loop has no visible seam and never repaints. A mask fades the horizon. Zero JS; fills its positioned parent.

- Import: `import RetroGrid from '@moonarc/core/RetroGrid'`
- Install: `npx astro add moonarc` · copy-paste: `npx shadcn@latest add https://moonarc.dev/r/retro-grid.json`
- Tier A · category background · trigger always
- Readout: `<RetroGrid angle={65} cellSize={60}>`
- Browser support: widely (Chrome 120 · Firefox 53 · Safari 15.4); elsewhere: the grid without the horizon fade
- Measured cost: 0 B JS (CSS 5.1 kB raw)
- Page: https://moonarc.dev/components/retro-grid/

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `angle` | `number` | `65` | Tilt of the floor in degrees; 65 reads as a floor, 45 as a ramp. |
| `cellSize` | `number` | `60` | Cell size in px; the perspective distance is four cells. |
| `color` | `string` | `ink at 22 %` | Line colour. |
| `speed` | `number` | `1` | Seconds per cell of travel. |
| `fade` | `boolean` | `true` | Fade out toward the horizon. |
| `lineWidth` | `number` | `1` | Line width in px. |
| `horizon` | `number` | `40` | Where the horizon sits, as a percentage of the height. |

## Usage

```astro
<section class="relative overflow-hidden">
  <RetroGrid />
  <h1 class="relative">Above the floor</h1>
</section>
```

## Reduced motion

The floor stands still.

## With ClientRouter

CSS-only; nothing to rebind.

## Craft

- Translate one cell and loop: the plane moves exactly its period, so the wrap is invisible and the animation is compositor-only; background-position would repaint every frame.
- The plane is hinged at the horizon (transform-origin at the top) and rotated back, with perspective set on the parent at four cells: near enough to converge, far enough that the top row is not a blur.
- Linear timing: an eased floor stutters at every loop.
- The lines are the ink token at low alpha, so the grid follows the theme without a second colour.

## Replaces

- RetroGrid (Magic UI)
- GridBackground (Aceternity)

## Source

```astro
---
/**
 * RetroGrid — a perspective grid floor sliding toward the viewer. Two
 * repeating gradients on one plane rotated in 3D; the motion is a
 * `translate` of exactly one cell on the compositor (not background-position,
 * which repaints every frame), so the loop is seamless and costs nothing
 * on the main thread. A mask fades the horizon. Fills its positioned parent.
 */
import type { HTMLAttributes } from 'astro/types';

interface Props extends HTMLAttributes<'div'> {
  /** Tilt of the floor in degrees; 65 is a floor, 45 a ramp. */
  angle?: number;
  /** Cell size in px. */
  cellSize?: number;
  /** Line colour. */
  color?: string;
  /** Seconds per cell of travel. */
  speed?: number;
  /** Fade out toward the horizon. */
  fade?: boolean;
  /** Line width in px. */
  lineWidth?: number;
  /** Where the horizon sits, as a percentage of the height. */
  horizon?: number;
}

const { angle = 65, cellSize = 60, color = 'color-mix(in srgb, var(--ma-ink) 22%, transparent)', speed = 1, fade = true, lineWidth = 1, horizon = 40, class: className, style, ...rest } = Astro.props;
const inline = [`--ma-retro-angle:${angle}deg`, `--ma-retro-cell:${cellSize}px`, `--ma-retro-color:${color}`, `--ma-retro-dur:${speed}s`, `--ma-retro-line:${lineWidth}px`, `--ma-retro-horizon:${horizon}%`, typeof style === 'string' ? style : '']
  .filter(Boolean)
  .join(';');
---

<div class:list={['ma-retro', className]} aria-hidden="true" data-fade={fade ? '' : undefined} style={inline} {...rest}>
  <div class="ma-retro__floor"><div class="ma-retro__lines"></div></div>
</div>

<style is:global>
  @layer components {
    :where(.ma-retro) {
      position: absolute;
      inset: 0;
      overflow: hidden;
      pointer-events: none;
      perspective: calc(var(--ma-retro-cell, 60px) * 4);
      contain: strict;
    }
    :where(.ma-retro[data-fade]) {
      mask-image: linear-gradient(to bottom, transparent var(--ma-retro-horizon, 40%), #000 calc(var(--ma-retro-horizon, 40%) + 30%));
    }
    /* the floor: a huge plane hinged at the horizon, rotated back */
    :where(.ma-retro__floor) {
      position: absolute;
      inset: var(--ma-retro-horizon, 40%) -100% -100%;
      transform-origin: 50% 0;
      transform: rotateX(var(--ma-retro-angle, 65deg));
    }
    :where(.ma-retro__lines) {
      position: absolute;
      inset: calc(-1 * var(--ma-retro-cell, 60px)) 0 0;
      background-image:
        linear-gradient(to right, var(--ma-retro-color) var(--ma-retro-line, 1px), transparent var(--ma-retro-line, 1px)),
        linear-gradient(to bottom, var(--ma-retro-color) var(--ma-retro-line, 1px), transparent var(--ma-retro-line, 1px));
      background-size: var(--ma-retro-cell, 60px) var(--ma-retro-cell, 60px);
      background-position: 50% 0;
      will-change: translate;
      animation: ma-retro var(--ma-retro-dur) linear infinite;
    }
    @media (prefers-reduced-motion: reduce) {
      :where(.ma-retro__lines) {
        animation: none;
      }
    }
  }

  @keyframes ma-retro {
    from {
      translate: 0 0;
    }
    to {
      translate: 0 var(--ma-retro-cell, 60px);
    }
  }
</style>

```
