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…

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 scrolls the tiled pattern by exactly one cell, so the loop is
 * seamless however the plane is tilted.
 */
const KEYFRAMES = `@keyframes zy-retro-grid{from{background-position:0 0}to{background-position:0 var(--zy-retro-cell)}}`

/** Alpha-only mask — keyword colors here, nothing is a rendered color. */
const HORIZON_MASK = "linear-gradient(to bottom, transparent, black 42%)"

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, top-0, w-[200%],
  h-[300%]) 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 background-position from 0 0
  to 0 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. (An alternative is animating translateY on the plane
  by half its height — cheaper for the compositor, same look.)
- 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 (0 var(--zy-retro-cell) -> 0 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 animates background-position 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.
  • 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 inklineColor 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