UI Components Easy
Retro Grid
A perspective grid background with vanishing-point effect, inspired by retro 80s aesthetics. CSS transforms create converging grid lines with a glowing horizon.
Open in Lab
MCP
css javascript vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
min-height: 100vh;
background: #000;
overflow: hidden;
}
/* --- Scene container --- */
.retro-scene {
--retro-color: rgba(139, 92, 246, 0.3);
--retro-size: 60px;
--retro-glow: rgba(139, 92, 246, 0.5);
position: relative;
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: linear-gradient(to bottom, #0a0a0a 0%, #0a0a0a 50%, #0d0520 100%);
overflow: hidden;
}
/* --- Horizon glow --- */
.retro-glow {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 120%;
height: 300px;
background: radial-gradient(ellipse 50% 80% at 50% 50%, var(--retro-glow), transparent);
opacity: 0.4;
pointer-events: none;
filter: blur(40px);
}
/* --- Grid wrapper (handles perspective) --- */
.retro-grid-wrapper {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 65%;
overflow: hidden;
perspective: 400px;
perspective-origin: 50% 0%;
}
/* --- The grid plane --- */
.retro-grid {
position: absolute;
inset: 0;
transform: rotateX(55deg);
transform-origin: 50% 0%;
background-image: repeating-linear-gradient(
90deg,
var(--retro-color) 0px,
var(--retro-color) 1px,
transparent 1px,
transparent var(--retro-size)
),
repeating-linear-gradient(
0deg,
var(--retro-color) 0px,
var(--retro-color) 1px,
transparent 1px,
transparent var(--retro-size)
);
background-size: var(--retro-size) var(--retro-size);
animation: retro-scroll 8s linear infinite;
}
@keyframes retro-scroll {
0% {
transform: rotateX(55deg) translateY(0);
}
100% {
transform: rotateX(55deg) translateY(var(--retro-size));
}
}
/* Fade grid at top edge (horizon) */
.retro-grid-wrapper::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 40%;
background: linear-gradient(to bottom, #0a0a0a 0%, transparent 100%);
pointer-events: none;
z-index: 1;
}
/* --- Content overlay --- */
.retro-content {
position: relative;
z-index: 2;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
margin-bottom: 10vh;
}
.retro-title {
font-size: clamp(3rem, 8vw, 6rem);
font-weight: 900;
letter-spacing: 0.05em;
line-height: 1;
color: #fafafa;
text-shadow: 0 0 40px rgba(139, 92, 246, 0.3), 0 0 80px rgba(139, 92, 246, 0.1);
}
.retro-accent {
color: #a78bfa;
}
.retro-subtitle {
font-size: 1rem;
font-weight: 400;
color: #71717a;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.retro-line {
width: 80px;
height: 2px;
background: linear-gradient(90deg, transparent, #8b5cf6, transparent);
margin-top: 0.5rem;
}// Retro Grid — the grid animation runs via CSS @keyframes.
// This script adds an optional parallax tilt on mouse movement.
(function () {
"use strict";
const scene = document.querySelector(".retro-scene");
const wrapper = document.querySelector(".retro-grid-wrapper");
if (!scene || !wrapper) return;
scene.addEventListener("mousemove", (e) => {
const rect = scene.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width - 0.5; // -0.5 to 0.5
const y = (e.clientY - rect.top) / rect.height - 0.5;
// Subtle shift of perspective origin based on mouse
const originX = 50 + x * 10;
const originY = y * 8;
wrapper.style.perspectiveOrigin = `${originX}% ${originY}%`;
});
scene.addEventListener("mouseleave", () => {
wrapper.style.perspectiveOrigin = "50% 0%";
});
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Retro Grid</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="retro-scene">
<!-- Horizon glow -->
<div class="retro-glow"></div>
<!-- Perspective grid plane -->
<div class="retro-grid-wrapper">
<div class="retro-grid"></div>
</div>
<!-- Content overlay -->
<div class="retro-content">
<h1 class="retro-title">RETRO<span class="retro-accent">GRID</span></h1>
<p class="retro-subtitle">Perspective grid with vanishing-point effect</p>
<div class="retro-line"></div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { type CSSProperties, type ReactNode, useCallback, useRef } from "react";
interface RetroGridProps {
/** Grid line color */
color?: string;
/** Grid cell size in pixels */
size?: number;
/** Horizon glow color */
glowColor?: string;
/** Animation duration in seconds (0 to disable) */
speed?: number;
/** Extra CSS class names */
className?: string;
/** Overlay children */
children?: ReactNode;
}
export function RetroGrid({
color = "rgba(139,92,246,0.3)",
size = 60,
glowColor = "rgba(139,92,246,0.5)",
speed = 8,
className = "",
children,
}: RetroGridProps) {
const wrapperRef = useRef<HTMLDivElement>(null);
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
const wrapper = wrapperRef.current;
if (!wrapper) return;
const rect = e.currentTarget.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width - 0.5;
const y = (e.clientY - rect.top) / rect.height - 0.5;
wrapper.style.perspectiveOrigin = `${50 + x * 10}% ${y * 8}%`;
}, []);
const handleMouseLeave = useCallback(() => {
const wrapper = wrapperRef.current;
if (wrapper) wrapper.style.perspectiveOrigin = "50% 0%";
}, []);
const animationCSS = speed
? `@keyframes retro-scroll{0%{transform:rotateX(55deg) translateY(0)}100%{transform:rotateX(55deg) translateY(${size}px)}}`
: "";
const sceneStyle: CSSProperties = {
position: "relative",
width: "100%",
height: "100vh",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
background: "linear-gradient(to bottom, #0a0a0a 0%, #0a0a0a 50%, #0d0520 100%)",
overflow: "hidden",
fontFamily: "system-ui, -apple-system, sans-serif",
};
const glowStyle: CSSProperties = {
position: "absolute",
left: "50%",
top: "50%",
transform: "translate(-50%, -50%)",
width: "120%",
height: 300,
background: `radial-gradient(ellipse 50% 80% at 50% 50%, ${glowColor}, transparent)`,
opacity: 0.4,
pointerEvents: "none",
filter: "blur(40px)",
};
const wrapperStyle: CSSProperties = {
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: "65%",
overflow: "hidden",
perspective: 400,
perspectiveOrigin: "50% 0%",
};
const gridStyle: CSSProperties = {
position: "absolute",
inset: 0,
transform: "rotateX(55deg)",
transformOrigin: "50% 0%",
backgroundImage: `repeating-linear-gradient(90deg, ${color} 0px, ${color} 1px, transparent 1px, transparent ${size}px), repeating-linear-gradient(0deg, ${color} 0px, ${color} 1px, transparent 1px, transparent ${size}px)`,
backgroundSize: `${size}px ${size}px`,
animation: speed ? `retro-scroll ${speed}s linear infinite` : "none",
};
const horizonFadeStyle: CSSProperties = {
position: "absolute",
top: 0,
left: 0,
right: 0,
height: "40%",
background: "linear-gradient(to bottom, #0a0a0a 0%, transparent 100%)",
pointerEvents: "none",
zIndex: 1,
};
const contentStyle: CSSProperties = {
position: "relative",
zIndex: 2,
};
return (
<div
className={className}
style={sceneStyle}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
{animationCSS && <style>{animationCSS}</style>}
<div style={glowStyle} />
<div ref={wrapperRef} style={wrapperStyle}>
<div style={gridStyle} />
<div style={horizonFadeStyle} />
</div>
<div style={contentStyle}>{children}</div>
</div>
);
}
// Demo usage
export default function RetroGridDemo() {
return (
<RetroGrid color="rgba(139,92,246,0.3)" size={60} speed={8}>
<div
style={{
textAlign: "center",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "0.75rem",
marginBottom: "10vh",
}}
>
<h1
style={{
fontSize: "clamp(3rem, 8vw, 6rem)",
fontWeight: 900,
letterSpacing: "0.05em",
lineHeight: 1,
color: "#fafafa",
fontFamily: "system-ui, -apple-system, sans-serif",
textShadow: "0 0 40px rgba(139,92,246,0.3), 0 0 80px rgba(139,92,246,0.1)",
}}
>
RETRO<span style={{ color: "#a78bfa" }}>GRID</span>
</h1>
<p
style={{
fontSize: "1rem",
fontWeight: 400,
color: "#71717a",
letterSpacing: "0.08em",
textTransform: "uppercase",
fontFamily: "system-ui, -apple-system, sans-serif",
}}
>
Perspective grid with vanishing-point effect
</p>
<div
style={{
width: 80,
height: 2,
background: "linear-gradient(90deg, transparent, #8b5cf6, transparent)",
marginTop: "0.5rem",
}}
/>
</div>
</RetroGrid>
);
}<script setup>
import { ref, computed } from "vue";
const props = defineProps({
color: { type: String, default: "rgba(139,92,246,0.3)" },
size: { type: Number, default: 60 },
glowColor: { type: String, default: "rgba(139,92,246,0.5)" },
speed: { type: Number, default: 8 },
});
const wrapperEl = ref(null);
const animationCSS = computed(() =>
props.speed
? `@keyframes retro-scroll{0%{transform:rotateX(55deg) translateY(0)}100%{transform:rotateX(55deg) translateY(${props.size}px)}}`
: ""
);
const glowStyle = computed(() => ({
position: "absolute",
left: "50%",
top: "50%",
transform: "translate(-50%, -50%)",
width: "120%",
height: "300px",
background: `radial-gradient(ellipse 50% 80% at 50% 50%, ${props.glowColor}, transparent)`,
opacity: 0.4,
pointerEvents: "none",
filter: "blur(40px)",
}));
const gridStyle = computed(() => ({
position: "absolute",
inset: 0,
transform: "rotateX(55deg)",
transformOrigin: "50% 0%",
backgroundImage: `repeating-linear-gradient(90deg, ${props.color} 0px, ${props.color} 1px, transparent 1px, transparent ${props.size}px), repeating-linear-gradient(0deg, ${props.color} 0px, ${props.color} 1px, transparent 1px, transparent ${props.size}px)`,
backgroundSize: `${props.size}px ${props.size}px`,
animation: props.speed ? `retro-scroll ${props.speed}s linear infinite` : "none",
}));
function handleMouseMove(e) {
const wrapper = wrapperEl.value;
if (!wrapper) return;
const rect = e.currentTarget.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width - 0.5;
const y = (e.clientY - rect.top) / rect.height - 0.5;
wrapper.style.perspectiveOrigin = `${50 + x * 10}% ${y * 8}%`;
}
function handleMouseLeave() {
const wrapper = wrapperEl.value;
if (wrapper) wrapper.style.perspectiveOrigin = "50% 0%";
}
</script>
<template>
<component :is="'style'" v-if="animationCSS">{{ animationCSS }}</component>
<div
style="position: relative; width: 100%; height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; background: linear-gradient(to bottom, #0a0a0a 0%, #0a0a0a 50%, #0d0520 100%); overflow: hidden; font-family: system-ui, -apple-system, sans-serif;"
@mousemove="handleMouseMove"
@mouseleave="handleMouseLeave"
>
<div :style="glowStyle" />
<div
ref="wrapperEl"
style="position: absolute; bottom: 0; left: 0; right: 0; height: 65%; overflow: hidden; perspective: 400px; perspective-origin: 50% 0%;"
>
<div :style="gridStyle" />
<div style="position: absolute; top: 0; left: 0; right: 0; height: 40%; background: linear-gradient(to bottom, #0a0a0a 0%, transparent 100%); pointer-events: none; z-index: 1;" />
</div>
<div style="position: relative; z-index: 2;">
<slot>
<div style="text-align: center; display: flex; flex-direction: column; align-items: center; gap: 0.75rem; margin-bottom: 10vh;">
<h1 style="font-size: clamp(3rem, 8vw, 6rem); font-weight: 900; letter-spacing: 0.05em; line-height: 1; color: #fafafa; font-family: system-ui, -apple-system, sans-serif; text-shadow: 0 0 40px rgba(139,92,246,0.3), 0 0 80px rgba(139,92,246,0.1);">
RETRO<span style="color: #a78bfa;">GRID</span>
</h1>
<p style="font-size: 1rem; font-weight: 400; color: #71717a; letter-spacing: 0.08em; text-transform: uppercase; font-family: system-ui, -apple-system, sans-serif;">
Perspective grid with vanishing-point effect
</p>
<div style="width: 80px; height: 2px; background: linear-gradient(90deg, transparent, #8b5cf6, transparent); margin-top: 0.5rem;" />
</div>
</slot>
</div>
</div>
</template><script>
export let color = "rgba(139,92,246,0.3)";
export let size = 60;
export let glowColor = "rgba(139,92,246,0.5)";
export let speed = 8;
let wrapperEl;
$: animationCSS = speed
? `@keyframes retro-scroll{0%{transform:rotateX(55deg) translateY(0)}100%{transform:rotateX(55deg) translateY(${size}px)}}`
: "";
$: gridStyle = `position: absolute; inset: 0; transform: rotateX(55deg); transform-origin: 50% 0%; background-image: repeating-linear-gradient(90deg, ${color} 0px, ${color} 1px, transparent 1px, transparent ${size}px), repeating-linear-gradient(0deg, ${color} 0px, ${color} 1px, transparent 1px, transparent ${size}px); background-size: ${size}px ${size}px; animation: ${speed ? `retro-scroll ${speed}s linear infinite` : "none"};`;
function handleMouseMove(e) {
if (!wrapperEl) return;
const rect = e.currentTarget.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width - 0.5;
const y = (e.clientY - rect.top) / rect.height - 0.5;
wrapperEl.style.perspectiveOrigin = `${50 + x * 10}% ${y * 8}%`;
}
function handleMouseLeave() {
if (wrapperEl) wrapperEl.style.perspectiveOrigin = "50% 0%";
}
</script>
<svelte:head>
{#if animationCSS}
{@html `<style>${animationCSS}</style>`}
{/if}
</svelte:head>
<div
style="position: relative; width: 100%; height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; background: linear-gradient(to bottom, #0a0a0a 0%, #0a0a0a 50%, #0d0520 100%); overflow: hidden; font-family: system-ui, -apple-system, sans-serif;"
on:mousemove={handleMouseMove}
on:mouseleave={handleMouseLeave}
>
<div style="position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 120%; height: 300px; background: radial-gradient(ellipse 50% 80% at 50% 50%, {glowColor}, transparent); opacity: 0.4; pointer-events: none; filter: blur(40px);" />
<div
bind:this={wrapperEl}
style="position: absolute; bottom: 0; left: 0; right: 0; height: 65%; overflow: hidden; perspective: 400px; perspective-origin: 50% 0%;"
>
<div style={gridStyle} />
<div style="position: absolute; top: 0; left: 0; right: 0; height: 40%; background: linear-gradient(to bottom, #0a0a0a 0%, transparent 100%); pointer-events: none; z-index: 1;" />
</div>
<div style="position: relative; z-index: 2;">
<slot>
<div style="text-align: center; display: flex; flex-direction: column; align-items: center; gap: 0.75rem; margin-bottom: 10vh;">
<h1 style="font-size: clamp(3rem, 8vw, 6rem); font-weight: 900; letter-spacing: 0.05em; line-height: 1; color: #fafafa; font-family: system-ui, -apple-system, sans-serif; text-shadow: 0 0 40px rgba(139,92,246,0.3), 0 0 80px rgba(139,92,246,0.1);">
RETRO<span style="color: #a78bfa;">GRID</span>
</h1>
<p style="font-size: 1rem; font-weight: 400; color: #71717a; letter-spacing: 0.08em; text-transform: uppercase; font-family: system-ui, -apple-system, sans-serif;">
Perspective grid with vanishing-point effect
</p>
<div style="width: 80px; height: 2px; background: linear-gradient(90deg, transparent, #8b5cf6, transparent); margin-top: 0.5rem;"></div>
</div>
</slot>
</div>
</div>Retro Grid
A perspective grid background with converging lines and a glowing horizon, inspired by retro 80s / synthwave aesthetics.
How it works
A flat grid is drawn using repeating linear-gradient (horizontal and vertical lines), then a CSS perspective + rotateX transform tilts it into 3D space. A gradient overlay creates the horizon glow effect where the lines converge. An optional CSS animation scrolls the grid for a moving-through-space illusion.
Customization
| Variable | Default | Description |
|---|---|---|
--retro-color | rgba(139,92,246,0.3) | Grid line color |
--retro-size | 60px | Grid cell size |
--retro-glow | rgba(139,92,246,0.5) | Horizon glow color |
When to use it
- Hero sections for creative or tech products
- Music/game-themed landing pages
- Retro-styled portfolios
- Conference or event pages