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…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/retro-grid.jsonPrompt
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+rotateXwith a toptransform-origin; the tilt alone creates the vanishing point, no per-line math and no canvas. - Seamless tile loop — the scroll animates
background-positionby 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 ink —
lineColornames 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.