freeNew102 lines · framer-motion
Retro Grid
A floor that never stops arriving. A full-width synthwave-style section: a perspective floor grid scrolls toward the viewer under a glowing horizon line, built entirely from CSS gradients and transforms, under a short glass-card headline.
- Use it for
- A bold, nostalgic section break for product launches, music or creative tools where a flat panel would feel too quiet.
- Themes
- Light + dark
- License
- Commercial use
Live
Horizon
A floor that never stops arriving
A perspective grid scrolling toward you under a glowing horizon.
tsx
"use client";
import * as React from "react";
import { motion, useInView, useReducedMotion } from "framer-motion";
import { cn } from "@/lib/utils";
function useReducedMotionSafe() {
const prefersReduced = useReducedMotion();
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => setMounted(true), []);
return mounted ? !!prefersReduced : false;
}
/**
* Full-width synthwave-style section: a perspective floor grid scrolls toward the
* viewer under a glowing horizon, built from CSS gradients and transforms (no canvas,
* colours follow CSS variables automatically). A glass card holds a short headline.
*/
export interface BgRetroGridProps {
eyebrow?: string;
headline?: string;
subcopy?: string;
/** Floor scroll speed multiplier. */
speed?: number;
/** Grid cell size in px. */
cellSize?: number;
/** Token used for the horizon glow. */
accent?: "signal" | "info" | "lime" | "ok";
className?: string;
}
export function BgRetroGrid({
eyebrow = "Horizon",
headline = "A floor that never stops arriving",
subcopy = "A perspective grid scrolling toward you under a glowing horizon.",
speed = 1,
cellSize = 44,
accent = "signal",
className,
}: BgRetroGridProps) {
const reduce = useReducedMotionSafe();
const hostRef = React.useRef<HTMLDivElement>(null);
const inView = useInView(hostRef, { amount: 0.15 });
const run = !reduce && inView;
return (
<section
ref={hostRef}
className={cn("relative isolate flex min-h-[22rem] items-center justify-center overflow-hidden bg-background sm:min-h-[26rem]", className)}
style={{ "--rg-accent": `var(--${accent})` } as React.CSSProperties}
>
<div
className="absolute inset-x-0 top-0 h-1/2"
style={{ background: "radial-gradient(ellipse 70% 100% at 50% 100%, color-mix(in srgb, var(--rg-accent) 35%, transparent), transparent 70%)" }}
aria-hidden
/>
<div className="absolute inset-x-0 top-[48%] h-px" style={{ background: "var(--rg-accent)", boxShadow: "0 0 24px var(--rg-accent)" }} aria-hidden />
<div className="absolute inset-x-0 bottom-0 h-1/2 overflow-hidden [perspective:420px]" aria-hidden>
<motion.div
className="absolute inset-x-[-50%] top-0 h-[200%] [transform-style:preserve-3d] [transform:rotateX(62deg)]"
style={{
backgroundImage:
"linear-gradient(color-mix(in srgb, var(--rg-accent) 55%, var(--foreground) 25%) 1.5px, transparent 1.5px), linear-gradient(90deg, color-mix(in srgb, var(--rg-accent) 55%, var(--foreground) 25%) 1.5px, transparent 1.5px)",
backgroundSize: `${cellSize}px ${cellSize}px`,
opacity: 0.75,
}}
initial={{ backgroundPositionY: "0px" }}
animate={run ? { backgroundPositionY: [`0px`, `${cellSize}px`] } : { backgroundPositionY: "0px" }}
transition={run ? { duration: 1.1 / speed, repeat: Infinity, ease: "linear" } : { duration: 0 }}
/>
<div
className="absolute inset-0"
style={{ background: "linear-gradient(to top, var(--background), transparent 32%)" }}
/>
</div>
<div className="absolute inset-0 bg-gradient-to-b from-background/10 via-background/0 to-background/10" aria-hidden />
<div className="relative mx-auto flex max-w-3xl flex-col items-center px-4 py-20 text-center sm:px-6">
<motion.div
initial={reduce ? false : { opacity: 0, y: 14 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.5 }}
transition={{ duration: 0.5 }}
className="rounded-[2rem] border border-border/60 bg-background/55 px-8 py-10 backdrop-blur-sm sm:px-14 sm:py-12"
>
{eyebrow && (
<span className="inline-flex items-center gap-2 rounded-full border border-border bg-card/80 px-3 py-1.5 font-mono text-[11px] uppercase tracking-[0.16em] text-muted-foreground">
<span className="size-1.5 rounded-full" style={{ background: "var(--rg-accent)" }} aria-hidden />
{eyebrow}
</span>
)}
<h2 className="mt-4 font-display text-[clamp(2rem,5vw,3.4rem)] font-extrabold leading-[1.03] tracking-[-0.03em]">{headline}</h2>
{subcopy && <p className="mt-4 max-w-xl text-base leading-relaxed text-muted-foreground sm:text-lg">{subcopy}</p>}
</motion.div>
</div>
</section>
);
}
export default BgRetroGrid;
How it behaves
- →The floor grid is a single transformed div (rotateX) with a repeating line-gradient background whose vertical position animates in a seamless one-cell loop, reading as an endless scroll toward the viewer.
- →A horizon glow and grid line colour both use the chosen accent token through a CSS custom property, so the whole scene re-colours instantly on theme change with no JS colour reading.
- →cellSize sets grid density; speed scales the scroll loop duration.
- →The loop only animates while the section is in view (framer-motion useInView) and freezes at a resting frame when prefers-reduced-motion is set.
Accessibility
- ✓The grid, glow and horizon line are aria-hidden; only the heading and copy are in the accessibility tree.
- ✓Headline sits on a translucent blurred card for contrast against the grid in both themes.
- ✓Honours prefers-reduced-motion.