Skip to content

Components / Backgrounds

Retro Grid

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.

Live demo

Ship it.

one cell per second · compositor

Live, from the library itself: scroll, hover, navigate away and back. The demo is never gated.

Measured

JavaScript of its own

0 B

CSS 5.1 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.

Browser support

Baseline · widely available

Chrome 120 · Firefox 53 · Safari 15.4; needs mask-image. Elsewhere: the grid without the horizon fade.

Install

One command adds the integration, the base tokens and every component. Then import what you use.

npx astro add moonarc
pnpm astro add moonarc
bunx astro add moonarc
src/pages/index.astro
---
import RetroGrid from '@moonarc/core/RetroGrid';
---
<section class="relative overflow-hidden">
  <RetroGrid />
  <h1 class="relative">Above the floor</h1>
</section>
Copy it into your project instead (shadcn registry)

Owns the file, no dependency. The registry item also installs the base tokens.

terminal
npx shadcn@latest add https://moonarc.dev/r/retro-grid.json

The CLI needs a components.json and the @/* alias, which Setup has. The file lands in src/components/moonarc/.

Source

The whole component. Self-contained styles in a cascade layer so your classes always win; it imports nothing.

RetroGrid.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>

Props

PropTypeDefaultDescription
anglenumber65Tilt of the floor in degrees; 65 reads as a floor, 45 as a ramp.
cellSizenumber60Cell size in px; the perspective distance is four cells.
colorstringink at 22 %Line colour.
speednumber1Seconds per cell of travel.
fadebooleantrueFade out toward the horizon.
lineWidthnumber1Line width in px.
horizonnumber40Where the horizon sits, as a percentage of the height.

Reduced motion

The floor stands still.

With ClientRouter

CSS-only; nothing to rebind.

Why it is built this way

Replaces: RetroGrid (Magic UI) · GridBackground (Aceternity). See the migration table.

Dot Pattern

Backgrounds

A field of dots behind content, faded toward the edges, with an optional slow pulse.

every browseralways

Grid Pattern

Backgrounds

A hairline grid behind content, faded toward the edges, optionally drifting one cell at a time.

every browseralways

Meteors

Backgrounds

Streaks falling diagonally across a twinkling sky.

every browseralways

Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown