Backgrounds

Retro Grid

An 80s synthwave floor — a tiled grid laid flat in perspective, scrolling toward the viewer and dissolving into the horizon.

Preview in your theme

Loading preview…

Registry dependencies
utils
import * as React from "react"
import { cn } from "@/lib/utils"

/**
 * Keyframes ship inside the component via React 19 hoisted <style> —
 * no tailwind config edits, and duplicates dedupe by href.
 * One cycle translates the tiled plane by exactly one cell, so the loop is
 * seamless however the plane is tilted. It animates transform, not
 * background-position, so the scroll runs on the compositor instead of
 * repainting the oversized plane every frame.
 */
const KEYFRAMES = `@keyframes zy-retro-grid{from{transform:translateY(0)}to{transform:translateY(var(--zy-retro-cell))}}`

/** Alpha-only mask — keyword colors here, nothing is a rendered color. */

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/retro-grid.json

Prompt

The prompt behind this component — paste it into your AI assistant to recreate or adapt it.

Build a React + TypeScript + Tailwind "RetroGrid" component — an 80s synthwave
perspective floor used as a background layer. Its only dependency is a cn()
class merger (clsx + tailwind-merge). No hooks, no events, no browser APIs, no
canvas — it must stay a server component, do NOT add "use client".

Contract
- export function RetroGrid(props): props extend
  Omit<React.ComponentProps<"div">, "children"> (rest props + ref spread onto
  the root div) plus:
  - angle?: number (default 35) — rotateX tilt of the floor in degrees. Higher
    tips the far end further away: stronger convergence, fewer visible rows.
    Useful range ~20–45; past ~50 the near edge crosses the perspective plane
    and the floor clips itself out of view entirely.
  - cellSize?: number (default 60) — grid cell size in px measured on the
    untilted plane.
  - speed?: number (default 3) — seconds for the grid to travel one cell
    toward the viewer.
  - opacity?: number (default 0.45) — opacity of the whole layer.
  - lineColor?: string (default "primary") — a theme token name WITHOUT the
    leading "--".
- It renders no children: it is a layer, not a wrapper. The consumer puts it in
  a `relative` ancestor and writes content in a `relative` sibling, which
  stacks above it with no z-index bookkeeping.

Behavior
- Root div: pointer-events-none absolute inset-0 overflow-hidden, aria-hidden.
  Inline style carries opacity plus three custom properties
  (--zy-retro-cell / --zy-retro-speed / --zy-retro-angle) with the consumer
  `style` spread last so every one is overridable per instance.
- Three nested layers, in this order and for these reasons:
  1) a mask layer (absolute inset-0) whose maskImage/WebkitMaskImage is
     linear-gradient(to bottom, transparent, black 42%) — the grid dissolves
     into the horizon at the top instead of ending on a hard line. Keep the
     mask OFF the element that owns `perspective`: masking that node flattens
     its 3D rendering context.
  2) the perspective node: [perspective:200px].
  3) the plane: [transform:rotateX(var(--zy-retro-angle))] with
     [transform-origin:50%_0%] — hinging at the top turns the plane into a
     floor receding upward to a vanishing point.
- Inside the plane sits ONE oversized div (-left-1/2, w-[200%], h-[300%],
  top at minus one cell: top-[calc(var(--zy-retro-cell)*-1)]) painted with two repeating linear-gradients — `to right` for the
  verticals and `to bottom` for the horizontals, each `<line> 1px, transparent
  1px` — at background-size var(--zy-retro-cell) square. Oversizing matters:
  once the near edge stretches under perspective the plane must still cover
  the frame edge to edge.
- Infinite forward scroll is one keyframe moving the tiled div's transform
  from translateY(0) to translateY(var(--zy-retro-cell)). Because the step
  equals exactly one tile the loop is seamless at any tilt, and perspective
  does the rest: near rows appear to race, far rows crawl. Starting the div one
  cell above the hinge keeps its top edge covering the horizon for the whole
  cycle. Animate transform, not background-position: background-position
  repaints the whole oversized plane every frame, transform stays on the
  compositor.
- The @keyframes ship inside the component via a React 19 hoisted
  <style href="zyeon-retro-grid" precedence="medium"> tag — no Tailwind config
  edits, and multiple instances dedupe to one style tag by href.
- prefers-reduced-motion: motion-reduce:[animation:none] on the plane. The
  perspective grid stays fully rendered and only stops scrolling — never hide
  it, a vanishing background leaves a blank panel.

Rendering & styling
- Semantic tokens only: the lines are var(--<lineColor>) — "primary" reads as
  ink on light themes and as a glowing wire on dark ones; "border" gives a
  quieter blueprint. No hex / rgb() / oklch() anywhere.
- The mask uses the `black`/`transparent` keywords, which are alpha values in
  a mask, not rendered colors.
- `opacity` is the single dimmer; keep line tokens at full strength and tune
  the layer instead, so the two knobs never fight.
- Merge consumer className via cn() on the root — the call site can retarget
  the layer (bottom half only: top-1/2), restack it (z-0) or round it.

Customization levers
- Horizon height: the 42% stop in the mask gradient — larger pushes the
  horizon down and shows less grid.
- Depth feel: the [perspective:200px] value paired with `angle`. They are
  coupled — the perspective distance sets the tilt at which the near edge
  crosses the camera and the whole floor vanishes (~50deg at 200px). Raise
  perspective to 300–400px if you want to push `angle` higher; 400px + 25deg
  is a subtly tilted plane.
- Rhythm: cellSize for density (40 px is arcade-tight, 90 px is architectural)
  and speed for pace (1–2 s is a chase, 6–10 s is ambient).
- Palette: lineColor takes any token name — "primary", "border", "chart-1",
  "destructive" for an alert floor.
- Glow: add a second copy of the plane behind the first with a blur filter and
  lower opacity for a neon bloom, or drop a radial-gradient sun above the
  horizon — both are one extra sibling div.
- Direction: reverse the keyframe (translateY(var(--zy-retro-cell)) ->
  translateY(0)) to make the grid recede away from the viewer.

Concepts

  • Perspective floor — a flat tiled plane plus perspective + rotateX with a top transform-origin; the tilt alone creates the vanishing point, no per-line math and no canvas.
  • Seamless tile loop — the scroll translates the tiled plane by exactly one cell, so the pattern lands back on itself every cycle and the motion has no visible seam at any tilt or cell size; animating transform instead of background-position keeps it on the compositor.
  • Horizon mask — an alpha-only gradient mask dissolves the far end of the plane instead of cutting it, letting the grid meet whatever surface it sits on rather than a hard edge.
  • Mask outside perspective — the mask is applied to a wrapper, not the perspective node, because masking an element flattens its 3D rendering context and would collapse the floor back to 2D.
  • Token-named ink — lineColor names a theme token rather than a color, so the same grid reads as blueprint ink on light themes and as a neon wire on dark ones.
  • Reduced-motion honesty — the grid stops scrolling but stays fully drawn under prefers-reduced-motion; the composition never disappears, only its speed does.

On This Page