retroGrid
A Backgrounds block/component from Magic UI — clean-room reimplemented for Domphy (see methodology). Call retroGrid() with no arguments for a working demo, or edit the code below live.
Props
| Prop | Type | Description |
|---|---|---|
angle | number | Floor tilt, in degrees. Defaults to 65. |
cellSize | number | Grid spacing, in px. Defaults to 60. |
opacity | number | Overall grid opacity. Defaults to 0.5. |
lightLineColor | ThemeColor | Line color family used under a light color scheme. Defaults to "neutral". |
darkLineColor | ThemeColor | Line color family used under prefers-color-scheme: dark. Defaults to "neutral". |
children | DomphyElement | DomphyElement[] | Foreground content layered above the grid. Defaults to a small demo heading. |
style | StyleObject | Passthrough style merged onto the outer container. |
Implementation notes
Uses the spec's own suggested simplification (explicitly called 'visually equivalent and far simpler to reimplement' in the research note) rather than the production fragment-shader ray-cast: two layered repeating-linear-gradients stand in for the tiled grid-line background image, inside a perspective + rotateX(angle) tilted plane, with a single linear-infinite @keyframes shifting background-position by exactly one grid tile per cycle for a seamless loop. A full-size top-to-bottom gradient overlay (fading the demo panel's own themeColor(l,'inherit') background to transparent) blends the horizon, and prefers-reduced-motion: reduce pauses the animation via animationPlayState (same pattern as this package's existing orbitingCircles.ts). Distance-based line thinning/LOD is intentionally out of scope, exactly as the spec itself allows ('a nice-to-have refinement'). lightLineColor/darkLineColor are both Domphy ThemeColor props (not literal colors) and are differentiated via a prefers-color-scheme: dark CSS media override -- an OS-level switch, since this standalone component has no hook into whatever app-level mechanism a consuming app uses to flip Domphy's own active theme; Domphy's themeColor() tone system already re-resolves per Domphy's active theme separately from this OS-level override.
Status: ported · Reference: Magic UI original
// Magic UI "Retro Grid" — clean-room reimplementation.
//
// A synthwave-style animated perspective floor grid that scrolls toward the
// viewer, typically used as a full-bleed hero background. Implemented purely
// from the block's public functional/visual spec — no upstream Magic UI
// source was viewed or copied.
//
// Per the spec's own research note, this uses the simplest faithful
// technique rather than the production version's per-pixel fragment-shader
// ray-cast: a large tiled `repeating-linear-gradient` "floor" plane (the
// lines are drawn as a CSS background image, not individual DOM/SVG
// elements) sits inside a 3D `perspective` container and is tilted with
// `rotateX(angle)` so it reads as a floor receding to a horizon. The
// "flying toward the camera" motion is a single `linear infinite` CSS
// `@keyframes` that shifts `background-position` by a whole number of grid
// tiles per cycle — matching upstream's brisk ~108px/s floor flow (its 300vh
// fallback plane translateY(-50%)->0 over 15s), independent of cell size — so
// the loop stays seamless with no JS animation frame needed. A
// full-size top-to-bottom gradient overlay div sits on top to fade the
// horizon into the surrounding surface color. Distant-line thinning/LOD (the
// production version's anti-aliasing refinement) is intentionally out of
// scope — see `fidelityNotes`.
//
// The spec's `lightLineColor`/`darkLineColor` props exist upstream because
// plain CSS custom properties don't auto-adapt without a duplicated
// `dark:` variant. Domphy's own `themeColor()` tone system already re-
// resolves per the active theme context, so both props are kept here purely
// for API fidelity and are additionally layered under a
// `prefers-color-scheme: dark` media override — see `fidelityNotes` for why
// that's an OS-level (not Domphy-theme-level) switch.
import type { DomphyElement, Listener, StyleObject } from "@domphy/core";
import { hashString } from "@domphy/core";
import { type ThemeColor, themeColor, themeSpacing } from "@domphy/theme";
import { heading, paragraph } from "@domphy/ui";
export interface RetroGridProps {
/** Floor tilt, in degrees. Defaults to `65`. */
angle?: number;
/** Grid spacing, in px. Defaults to `60`. */
cellSize?: number;
/** Overall grid opacity. Defaults to `0.5`. */
opacity?: number;
/** Line color family used under a light color scheme. Defaults to `"neutral"`. */
lightLineColor?: ThemeColor;
/** Line color family used under `prefers-color-scheme: dark`. Defaults to `"neutral"`. */
darkLineColor?: ThemeColor;
/** Foreground content layered above the grid. Defaults to a small demo heading. */
children?: DomphyElement | DomphyElement[];
/** Passthrough style merged onto the outer container. */
style?: StyleObject;
}
let retroGridInstanceCounter = 0;
// Upstream scrolls the floor ~1.5 viewport-heights per 15s cycle (300vh plane,
// translateY(-50%)->0) — ~108px/s at a 1080px viewport, i.e. ~1.8 cells/sec at
// the default cellSize. Reproduce that px/s here, independent of cellSize (as
// upstream is), while keeping the background-position loop seamless.
// ponytail: 1620 = 1.5 * a nominal 1080px viewport; bump if the target differs.
const SCROLL_DISTANCE_PX = 1620;
/** Multi-tile-per-cycle `background-position` scroll, seamless because it always ends a whole number of tiles past where it started. */
function buildScrollKeyframes(cellSize: number) {
const tilesPerCycle = Math.max(1, Math.round(SCROLL_DISTANCE_PX / cellSize));
const distance = tilesPerCycle * cellSize;
return {
"0%": { backgroundPosition: "0 0" },
"100%": { backgroundPosition: `0 ${distance}px` },
};
}
/** Two layered `repeating-linear-gradient`s (horizontal + vertical hairlines) standing in for a tiled grid-line background image. */
function buildGridBackgroundImage(lineColor: string, cellSize: number): string {
const vertical = `repeating-linear-gradient(90deg, ${lineColor} 0, ${lineColor} 1px, transparent 1px, transparent ${cellSize}px)`;
const horizontal = `repeating-linear-gradient(0deg, ${lineColor} 0, ${lineColor} 1px, transparent 1px, transparent ${cellSize}px)`;
return `${vertical}, ${horizontal}`;
}
/**
* A synthwave-style perspective floor grid, continuously scrolling toward
* the viewer, meant as a full-bleed hero background. Call with no arguments
* for a working demo — a white panel with the scrolling gray grid behind a
* heading.
*/
function retroGrid(props: RetroGridProps = {}): DomphyElement<"div"> {
const angle = Math.min(Math.max(props.angle ?? 65, 1), 89);
const cellSize = Math.max(1, props.cellSize ?? 60);
const opacity = props.opacity ?? 0.5;
const lightLineColor = props.lightLineColor ?? "neutral";
const darkLineColor = props.darkLineColor ?? "neutral";
const instanceId = ++retroGridInstanceCounter;
const scrollKeyframes = buildScrollKeyframes(cellSize);
const scrollAnimationName = `retro-grid-scroll-${hashString(
JSON.stringify({ instanceId, scrollKeyframes }),
)}`;
const contentChildren: DomphyElement[] = props.children
? Array.isArray(props.children)
? props.children
: [props.children]
: [
{ h2: "Retro Grid", $: [heading()] } as DomphyElement,
{
p: "A synthwave floor grid scrolling endlessly toward the viewer.",
$: [paragraph()],
} as DomphyElement,
];
const floorPlane: DomphyElement = {
div: null,
ariaHidden: "true",
// Decorative background-image plane, no text of its own.
_doctorDisable: "missing-color",
style: {
position: "absolute",
top: 0,
left: "-50%",
width: "200%",
height: "200%",
transform: `rotateX(${angle}deg)`,
transformOrigin: "top",
backgroundImage: (listener: Listener) =>
buildGridBackgroundImage(
themeColor(listener, "shift-6", lightLineColor),
cellSize,
),
backgroundSize: `${cellSize}px ${cellSize}px`,
animation: `${scrollAnimationName} 15s linear infinite`,
[`@keyframes ${scrollAnimationName}`]: scrollKeyframes,
"@media (prefers-color-scheme: dark)": {
backgroundImage: (listener: Listener) =>
buildGridBackgroundImage(
themeColor(listener, "shift-9", darkLineColor),
cellSize,
),
},
"@media (prefers-reduced-motion: reduce)": {
animationPlayState: "paused",
},
} as StyleObject,
} as DomphyElement;
const horizonOverlay: DomphyElement = {
div: null,
ariaHidden: "true",
// Decorative fade overlay, no text of its own.
_doctorDisable: "missing-color",
style: {
position: "absolute",
inset: 0,
backgroundImage: (listener: Listener) =>
`linear-gradient(to top, ${themeColor(listener, "inherit")} 0%, transparent 90%)`,
} as StyleObject,
} as DomphyElement;
const perspectiveWrapper: DomphyElement = {
div: [floorPlane, horizonOverlay],
ariaHidden: "true",
style: {
position: "absolute",
inset: 0,
overflow: "hidden",
pointerEvents: "none",
opacity,
// A 3D `perspective` distance needs to be large relative to the plane
// it's projecting, or points past that distance get crushed toward (or
// past) the vanishing point. `floorPlane` is 200% of this wrapper's own
// height, tilted `rotateX(angle)deg` from its top edge — at `200px`
// (this component's previous value) that depth vastly exceeds the
// perspective distance, so the whole plane collapsed into an ~60px
// sliver at the very top of the container instead of fanning out
// through it (confirmed via getBoundingClientRect: rendered height
// dropped from the expected several-hundred px down to ~61px). `1000px`
// keeps the projected floor spanning the full container at the default
// `angle`/size this block ships with.
perspective: "1000px",
} as StyleObject,
} as DomphyElement;
return {
div: [
perspectiveWrapper,
{ div: contentChildren, style: { position: "relative", zIndex: 1 } },
],
dataTone: "shift-0",
style: {
position: "relative",
overflow: "hidden",
borderRadius: themeSpacing(4),
padding: themeSpacing(8),
minHeight: themeSpacing(64),
backgroundColor: (listener: Listener) => themeColor(listener, "inherit"),
color: (listener: Listener) => themeColor(listener, "shift-9"),
...(props.style ?? {}),
} as StyleObject,
};
}
export { retroGrid };