Retro Forge
A synthwave perspective grid reimagined as a forge floor: a near-black plane receding to the horizon with ember-red scrolling grid lines, a molten-amber furnace glow, and an optional slit-scanned horizon sun. Pure CSS, SSR-safe, honors prefers-reduced-motion.
cssfree
import * as React from "react";
import { cn } from "@/lib/utils";
export interface RetroForgeProps extends React.ComponentPropsWithoutRef<"div"> {
/**
* Three forge tones: `[line, glow, sun]`. `line` is the grid-line color
* (ember red), `glow` is the horizon furnace-glow (molten amber), `sun` is
* the horizon semicircle. Any CSS color.
*/
colors?: [string, string, string];
/** Grid-scroll speed multiplier. 1 = default, 2 = twice as fast. Default 1. */
speed?: number;
/**
* Heat 0–1: scales the brightness of the horizon glow, the moving band, and
* the sun together. Default 1.
*/
heat?: number;
/** Grid cell size in px (larger = coarser grid). Default 60. */
cellSize?: number;
/** Render the horizon "sun" semicircle with scanline slits. Default true. */
sun?: boolean;
/** Freeze the animation at its current frame. */
paused?: boolean;
}
const DEFAULT_COLORS: [string, string, string] = [
"#c1121f", // ember red — grid lines
"#ff6b35", // molten amber — horizon glow
"#ff8c42", // molten orange — sun
];
/**
* Retro Forge — a synthwave perspective grid reimagined as a forge floor.
* A near-black plane recedes to the horizon in 3D, ember-red grid lines scroll
* toward the viewer, a molten-amber furnace glow lights the horizon (never the
* usual purple), and an optional slit-scanned "sun" sits on the horizon line.
* Pure CSS — transforms, gradients and keyframes, zero JS beyond React.
* Position it inside any `relative` container; it fills and sits behind content.
*/
export function RetroForge({
colors = DEFAULT_COLORS,
speed = 1,
heat = 1,
cellSize = 60,
sun = true,
paused = false,
className,
style,
...props
}: RetroForgeProps) {
const dur = (base: number) => `${base / Math.max(speed, 0.01)}s`;
const clampedHeat = Math.min(Math.max(heat, 0), 1);
return (
<div
aria-hidden
data-crucible="retro-forge"
className={cn("absolute inset-0 -z-10 overflow-hidden bg-neutral-950", className)}
style={
{
"--rf-line": colors[0],
"--rf-glow": colors[1],
"--rf-sun": colors[2],
"--rf-cell": `${cellSize}px`,
"--rf-heat": clampedHeat,
"--rf-play": paused ? "paused" : "running",
perspective: "340px",
perspectiveOrigin: "50% 0%",
...style,
} as React.CSSProperties
}
{...props}
>
<style>{`
@keyframes crucible-rf-scroll {
0% { background-position: 0 0, 0 0; }
100% { background-position: 0 var(--rf-cell), 0 var(--rf-cell); }
}
@keyframes crucible-rf-band {
0% { transform: translateY(-40%); opacity: 0; }
15% { opacity: 1; }
85% { opacity: 1; }
100% { transform: translateY(120%); opacity: 0; }
}
@keyframes crucible-rf-pulse {
0%, 100% { opacity: calc(0.55 * var(--rf-heat)); }
50% { opacity: calc(0.9 * var(--rf-heat)); }
}
[data-crucible="retro-forge"] .rf-anim {
animation-play-state: var(--rf-play);
}
@media (prefers-reduced-motion: reduce) {
[data-crucible="retro-forge"] .rf-anim { animation: none; }
}
`}</style>
{/* Furnace-glow horizon: a molten-amber wash sitting on the horizon line. */}
<div
className="absolute inset-x-0 top-1/2 h-1/2"
style={{
background:
"radial-gradient(120% 90% at 50% 0%, color-mix(in oklab, var(--rf-glow) 70%, transparent) 0%, color-mix(in oklab, var(--rf-glow) 22%, transparent) 32%, transparent 62%)",
opacity: 0.85 * clampedHeat,
}}
/>
{/* Optional horizon sun with scanline slits, clipped to a semicircle. */}
{sun ? (
<div
className="absolute left-1/2 top-1/2 h-40 w-40 -translate-x-1/2 -translate-y-full overflow-hidden"
style={{ clipPath: "inset(0 0 50% 0)" }}
>
<div
className="rf-anim h-40 w-40 rounded-full"
style={{
background:
"linear-gradient(180deg, color-mix(in oklab, var(--rf-sun) 95%, white 12%) 0%, var(--rf-sun) 45%, var(--rf-glow) 100%)",
WebkitMaskImage:
"repeating-linear-gradient(180deg, #000 0 6px, transparent 6px 11px)",
maskImage:
"repeating-linear-gradient(180deg, #000 0 6px, transparent 6px 11px)",
filter: "drop-shadow(0 0 22px color-mix(in oklab, var(--rf-sun) 60%, transparent))",
animation: "crucible-rf-pulse 3.4s ease-in-out infinite",
}}
/>
</div>
) : null}
{/* The receding grid plane. */}
<div
className="absolute inset-x-0 top-1/2 bottom-0"
style={{
transformStyle: "preserve-3d",
transform: "rotateX(78deg)",
transformOrigin: "center top",
}}
>
{/* Scrolling grid lines. */}
<div
className="rf-anim absolute inset-0"
style={{
backgroundImage:
"repeating-linear-gradient(90deg, color-mix(in oklab, var(--rf-line) 85%, transparent) 0 1px, transparent 1px var(--rf-cell)), repeating-linear-gradient(0deg, color-mix(in oklab, var(--rf-line) 85%, transparent) 0 1px, transparent 1px var(--rf-cell))",
backgroundSize: "var(--rf-cell) var(--rf-cell), var(--rf-cell) var(--rf-cell)",
maskImage: "linear-gradient(180deg, transparent 0%, #000 22%, #000 100%)",
WebkitMaskImage: "linear-gradient(180deg, transparent 0%, #000 22%, #000 100%)",
animation: `crucible-rf-scroll ${dur(2.6)} linear infinite`,
}}
/>
{/* Molten band rolling toward the viewer over the plane. */}
<div
className="rf-anim absolute inset-x-0 h-1/3"
style={{
background:
"linear-gradient(180deg, transparent 0%, color-mix(in oklab, var(--rf-glow) 55%, transparent) 50%, transparent 100%)",
mixBlendMode: "screen",
opacity: clampedHeat,
animation: `crucible-rf-band ${dur(5)} ease-in-out infinite`,
}}
/>
</div>
{/* Vignette so the plane sinks into black at the frame edges. */}
<div
className="absolute inset-0"
style={{
background:
"radial-gradient(120% 80% at 50% 60%, transparent 40%, rgba(0,0,0,0.55) 100%)",
}}
/>
</div>
);
}
Installation
CLI
npx shadcn@latest add @crucible/retro-forgeHonors prefers-reduced-motion with a designed static fallback, and passes className through.