19 / 20
All 20 in 20 CSS Neon Effects
- 01CSS Glow Anatomy with Layered Box Shadow
- 02Drop Shadow Glow for Irregular Shapes
- 03Neon Color Choice and Perceived Luminance
- 04Inner Glow and Faked Light Emission
- 05Animating Glow Intensity with at-property
- 06Glow on SVG Paths and Stroke Elements
- 07Neon Glow Paint Cost and What to Animate
- 08Accessible Neon on Dark Backgrounds
- 09Neon Glow Button Construction Reference
- 10Neon Heading and Display Type Glow
- 11Neon Pricing Table with Glow Tiers
- 12Layered Glow Scene with Depth Falloff
- 13Neon Form Inputs and Focus Glow
- 14Neon Navigation with Active Glow State
- 15Flicker Timing and Failing Tube Effect
- 16Cyberpunk Neon Palette and Token Set
- 17Tailwind Neon Glow Utility Recipe
- 18Neon Icon Glow with SVG Filters
- 19Infinite Neon Grid and Perspective Floor
- 20Multi Hue Neon Gradient Glow
Infinite Neon Grid and Perspective Floor
A perspective floor treated as geometry plus falloff: a repeating grid on a rotated plane, translated by exactly one cell per loop so the motion is seamless, with glow fading toward the horizon via a mask rather than by dimming the lines. Speed and density are adjustable, and the whole thing animates one transform.
Untitled UI Pro: a 10,000+ component Figma UI kit · from $129 one-time
Untitled UI · Affiliate link
The code
<section class="nn-19" aria-labelledby="nn-19-heading">
<div class="nn-19__stage">
<div class="nn-19__theme">
<input class="nn-19__tin" type="checkbox" id="nn-19-theme">
<label class="nn-19__tlab" for="nn-19-theme">
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M8 1.5v2M8 12.5v2M1.5 8h2M12.5 8h2M3.4 3.4l1.4 1.4M11.2 11.2l1.4 1.4M12.6 3.4l-1.4 1.4M4.8 11.2l-1.4 1.4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path><circle cx="8" cy="8" r="2.6" fill="none" stroke="currentColor" stroke-width="1.5"></circle></svg>
Flip light / dark
</label>
</div>
<header class="nn-19__head">
<p class="nn-19__kicker">Geometry and falloff</p>
<h2 class="nn-19__heading" id="nn-19-heading">A floor that loops on exactly one cell</h2>
<p class="nn-19__sub">One transform animation, one gradient, one mask. The seam disappears because the loop distance is the grid pitch.</p>
<div class="nn-19__controls">
<button class="nn-19__btn" type="button" data-nn19-speed="slow">Slow</button>
<button class="nn-19__btn" type="button" data-nn19-speed="normal" aria-pressed="true">Normal</button>
<button class="nn-19__btn" type="button" data-nn19-speed="fast">Fast</button>
<button class="nn-19__btn nn-19__btn--alt" type="button" id="nn-19-density">Density: 3rem</button>
</div>
</header>
<div class="nn-19__frame">
<div class="nn-19__sky"></div>
<span class="nn-19__horizon"></span>
<div class="nn-19__floor"><div class="nn-19__plane"></div></div>
<div class="nn-19__vignette"></div>
</div>
<dl class="nn-19__facts">
<div><dt>loop</dt><dd>translate3d(0, one cell, 0)</dd></div>
<div><dt>plane</dt><dd>rotateX(74deg), perspective 12rem</dd></div>
<div><dt>falloff</dt><dd>mask-image fade toward the horizon</dd></div>
<div><dt>animated</dt><dd>transform only</dd></div>
</dl>
</div>
</section><section class="nn-19" aria-labelledby="nn-19-heading">
<div class="nn-19__stage">
<div class="nn-19__theme">
<input class="nn-19__tin" type="checkbox" id="nn-19-theme">
<label class="nn-19__tlab" for="nn-19-theme">
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M8 1.5v2M8 12.5v2M1.5 8h2M12.5 8h2M3.4 3.4l1.4 1.4M11.2 11.2l1.4 1.4M12.6 3.4l-1.4 1.4M4.8 11.2l-1.4 1.4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path><circle cx="8" cy="8" r="2.6" fill="none" stroke="currentColor" stroke-width="1.5"></circle></svg>
Flip light / dark
</label>
</div>
<header class="nn-19__head">
<p class="nn-19__kicker">Geometry and falloff</p>
<h2 class="nn-19__heading" id="nn-19-heading">A floor that loops on exactly one cell</h2>
<p class="nn-19__sub">One transform animation, one gradient, one mask. The seam disappears because the loop distance is the grid pitch.</p>
<div class="nn-19__controls">
<button class="nn-19__btn" type="button" data-nn19-speed="slow">Slow</button>
<button class="nn-19__btn" type="button" data-nn19-speed="normal" aria-pressed="true">Normal</button>
<button class="nn-19__btn" type="button" data-nn19-speed="fast">Fast</button>
<button class="nn-19__btn nn-19__btn--alt" type="button" id="nn-19-density">Density: 3rem</button>
</div>
</header>
<div class="nn-19__frame">
<div class="nn-19__sky"></div>
<span class="nn-19__horizon"></span>
<div class="nn-19__floor"><div class="nn-19__plane"></div></div>
<div class="nn-19__vignette"></div>
</div>
<dl class="nn-19__facts">
<div><dt>loop</dt><dd>translate3d(0, one cell, 0)</dd></div>
<div><dt>plane</dt><dd>rotateX(74deg), perspective 12rem</dd></div>
<div><dt>falloff</dt><dd>mask-image fade toward the horizon</dd></div>
<div><dt>animated</dt><dd>transform only</dd></div>
</dl>
</div>
</section>.nn-19 {
--nn-19-neons: #ff5ec4;
--nn-19-bad: #f4a4a4;
--nn-19-warn: #f0c88a;
--nn-19-spec-neon: #ff5ec4;
}
@media (prefers-color-scheme: light) {
.nn-19 {
--nn-19-bad: #a12a20;
--nn-19-warn: #78490a;
}
.nn-19:has(#nn-19-theme:checked) {
--nn-19-bad: #f4a4a4;
--nn-19-warn: #f0c88a;
}
}
@media (prefers-color-scheme: dark) {
.nn-19:has(#nn-19-theme:checked) {
--nn-19-bad: #a12a20;
--nn-19-warn: #78490a;
}
}
.nn-19[data-theme="light"] {
--nn-19-bad: #a12a20;
--nn-19-warn: #78490a;
}
.nn-19 {
--nn-19-spec-ink: #f4eefb;
--nn-19-spec-dim: #a294c4;
}
.nn-19__theme {
position: absolute;
inset-block-start: clamp(10px, 2vw, 18px);
inset-inline-end: clamp(10px, 2vw, 18px);
z-index: 5;
display: flex;
justify-content: flex-end;
}
.nn-19__tin {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
}
.nn-19__tlab {
display: inline-flex;
align-items: center;
gap: .4rem;
min-height: 44px;
padding: 0 .8rem;
border: 1px solid currentColor;
font-size: .7rem;
letter-spacing: .12em;
text-transform: uppercase;
cursor: pointer;
opacity: .7;
}
.nn-19__tlab svg {
width: .9rem;
height: .9rem;
flex: none;
}
.nn-19__tlab:hover {
opacity: 1;
}
.nn-19__tin:focus-visible + .nn-19__tlab {
outline: 2px solid currentColor;
outline-offset: 2px;
}
@media (prefers-color-scheme: light) {
.nn-19:has(#nn-19-theme:checked) {
--nn-19-bg: #080617;
--nn-19-line: #221a44;
--nn-19-ink: #f4eefb;
--nn-19-muted: #a294c4;
--nn-19-neon: #ff5ec4;
}
}
@media (prefers-color-scheme: dark) {
.nn-19:has(#nn-19-theme:checked) {
--nn-19-bg: #eceaf4;
--nn-19-line: #c9c2dc;
--nn-19-ink: #120c22;
--nn-19-muted: #4f4570;
--nn-19-neon: #a3126f;
}
}
.nn-19 {
--nn-19-hue: 330;
--nn-19-cell: 3rem;
--nn-19-dur: 2.4s;
--nn-19-bg: #080617;
--nn-19-line: #221a44;
--nn-19-ink: #f4eefb;
--nn-19-muted: #a294c4;
--nn-19-neon: #ff5ec4;
--nn-19-core: rgba(255, 94, 196, .9);
--nn-19-halo: rgba(255, 94, 196, .4);
--nn-19-font-display: "Avenir Next Condensed", "Roboto Condensed", "Arial Narrow", system-ui, sans-serif;
width: 100%;
min-height: 100vh;
position: relative;
display: block;
box-sizing: border-box;
background: var(--nn-19-bg);
color: var(--nn-19-ink);
font-family: system-ui, sans-serif;
}
.nn-19 *,
.nn-19 *::before,
.nn-19 *::after {
box-sizing: border-box;
}
@supports (color: oklch(50% 0.1 200)) {
.nn-19 {
--nn-19-bg: oklch(13% 0.04 290);
--nn-19-line: oklch(28% 0.06 290);
--nn-19-ink: oklch(96% 0.016 320);
--nn-19-muted: oklch(73% 0.04 300);
--nn-19-neon: oklch(76% 0.21 var(--nn-19-hue));
--nn-19-core: oklch(78% 0.21 var(--nn-19-hue) / .9);
--nn-19-halo: oklch(74% 0.2 var(--nn-19-hue) / .4);
}
}
@media (prefers-color-scheme: light) {
.nn-19 {
--nn-19-bg: #eceaf4;
--nn-19-line: #c9c2dc;
--nn-19-ink: #120c22;
--nn-19-muted: #4f4570;
--nn-19-neon: #a3126f;
}
}
.nn-19[data-theme="light"] {
--nn-19-bg: #eceaf4;
--nn-19-line: #c9c2dc;
--nn-19-ink: #120c22;
--nn-19-muted: #4f4570;
--nn-19-neon: #a3126f;
}
.nn-19[data-theme="dark"] {
--nn-19-bad: #f4a4a4;
--nn-19-warn: #f0c88a;
--nn-19-bg: #080617;
--nn-19-line: #221a44;
--nn-19-ink: #f4eefb;
--nn-19-muted: #a294c4;
--nn-19-neon: #ff5ec4;
}
.nn-19[data-speed="slow"] {
--nn-19-dur: 4.5s;
}
.nn-19[data-speed="fast"] {
--nn-19-dur: 1.2s;
}
.nn-19[data-density="tight"] {
--nn-19-cell: 1.75rem;
}
.nn-19[data-density="wide"] {
--nn-19-cell: 4.5rem;
}
.nn-19__stage {
max-width: 68rem;
margin: 0 auto;
padding: clamp(1.75rem, 5vw, 2.5rem) clamp(1.25rem, 5vw, 2.25rem) clamp(2.25rem, 6vw, 3rem);
display: grid;
gap: clamp(1.5rem, 4vw, 2rem);
}
.nn-19__kicker {
margin: 0 0 .45rem;
font-family: var(--nn-19-font-display);
font-size: .82rem;
letter-spacing: .22em;
text-transform: uppercase;
color: var(--nn-19-neon);
}
.nn-19__heading {
margin: 0;
font-family: var(--nn-19-font-display);
font-size: clamp(1.5rem, 5.5vw, 2.4rem);
font-weight: 600;
text-transform: uppercase;
letter-spacing: .02em;
text-wrap: balance;
}
.nn-19__sub {
margin: .55rem 0 0;
max-width: 42rem;
font-size: .9rem;
line-height: 1.6;
color: var(--nn-19-muted);
text-wrap: pretty;
}
.nn-19__controls {
margin-top: .9rem;
display: flex;
gap: .4rem;
flex-wrap: wrap;
}
.nn-19__btn {
min-height: 44px;
padding: 0 .9rem;
border: 1px solid var(--nn-19-line);
border-radius: .2rem;
background: transparent;
color: var(--nn-19-muted);
font-family: var(--nn-19-font-display);
font-size: .8rem;
letter-spacing: .08em;
text-transform: uppercase;
cursor: pointer;
}
.nn-19__btn:hover {
color: var(--nn-19-ink);
border-color: var(--nn-19-neon);
}
.nn-19__btn[aria-pressed="true"] {
color: var(--nn-19-ink);
border-color: var(--nn-19-neon);
box-shadow: 0 0 .35rem var(--nn-19-halo);
}
.nn-19__btn:focus-visible {
outline: 2px solid var(--nn-19-ink);
outline-offset: 2px;
}
.nn-19__frame {
position: relative;
isolation: isolate;
overflow: hidden;
min-height: clamp(16rem, 48vw, 26rem);
border: 1px solid var(--nn-19-line);
background: linear-gradient(180deg, #0b0824 0%, #080617 48%, #050411 100%);
}
.nn-19__sky {
position: absolute;
inset: 0 0 52% 0;
background: radial-gradient(70% 120% at 50% 100%, color-mix(in srgb, var(--nn-19-neons) 22%, transparent), transparent 70%);
}
.nn-19__horizon {
position: absolute;
left: 0;
right: 0;
top: 48%;
height: 1px;
background: var(--nn-19-neons);
box-shadow: 0 0 .35rem var(--nn-19-core), 0 0 clamp(1.5rem, 7vw, 3rem) var(--nn-19-halo);
}
.nn-19__floor {
position: absolute;
inset: 48% 0 0 0;
perspective: 12rem;
perspective-origin: 50% 0%;
overflow: hidden;
}
.nn-19__plane {
position: absolute;
left: -50%;
right: -50%;
top: 0;
height: 300%;
transform-origin: 50% 0%;
transform: rotateX(74deg);
background-image: repeating-linear-gradient(90deg, var(--nn-19-neons) 0 1px, transparent 1px var(--nn-19-cell)), repeating-linear-gradient(180deg, var(--nn-19-neons) 0 1px, transparent 1px var(--nn-19-cell));
-webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 22%, #000 100%);
mask-image: linear-gradient(180deg, transparent 0%, #000 22%, #000 100%);
animation: nn-19-scroll var(--nn-19-dur) linear infinite;
}
@keyframes nn-19-scroll {
from {
transform: rotateX(74deg) translate3d(0, 0, 0);
}
to {
transform: rotateX(74deg) translate3d(0, var(--nn-19-cell), 0);
}
}
.nn-19__vignette {
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(110% 80% at 50% 48%, transparent 45%, rgba(3, 2, 10, .8) 100%);
}
.nn-19__facts {
margin: 0;
display: grid;
gap: .4rem;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
}
.nn-19__facts div {
display: grid;
gap: .15rem;
padding: .5rem .6rem;
border: 1px solid var(--nn-19-line);
min-inline-size: 0;
}
.nn-19__facts dt {
font-family: var(--nn-19-font-display);
font-size: .68rem;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--nn-19-muted);
}
.nn-19__facts dd {
margin: 0;
font-family: ui-monospace, Menlo, monospace;
font-size: .72rem;
color: var(--nn-19-ink);
}
@media (prefers-reduced-motion: reduce) {
.nn-19__plane {
animation: none;
}
}.nn-19 {
--nn-19-neons: #ff5ec4;
--nn-19-bad: #f4a4a4;
--nn-19-warn: #f0c88a;
--nn-19-spec-neon: #ff5ec4;
}
@media (prefers-color-scheme: light) {
.nn-19 {
--nn-19-bad: #a12a20;
--nn-19-warn: #78490a;
}
.nn-19:has(#nn-19-theme:checked) {
--nn-19-bad: #f4a4a4;
--nn-19-warn: #f0c88a;
}
}
@media (prefers-color-scheme: dark) {
.nn-19:has(#nn-19-theme:checked) {
--nn-19-bad: #a12a20;
--nn-19-warn: #78490a;
}
}
.nn-19[data-theme="light"] {
--nn-19-bad: #a12a20;
--nn-19-warn: #78490a;
}
.nn-19 {
--nn-19-spec-ink: #f4eefb;
--nn-19-spec-dim: #a294c4;
}
.nn-19__theme {
position: absolute;
inset-block-start: clamp(10px, 2vw, 18px);
inset-inline-end: clamp(10px, 2vw, 18px);
z-index: 5;
display: flex;
justify-content: flex-end;
}
.nn-19__tin {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
}
.nn-19__tlab {
display: inline-flex;
align-items: center;
gap: .4rem;
min-height: 44px;
padding: 0 .8rem;
border: 1px solid currentColor;
font-size: .7rem;
letter-spacing: .12em;
text-transform: uppercase;
cursor: pointer;
opacity: .7;
}
.nn-19__tlab svg {
width: .9rem;
height: .9rem;
flex: none;
}
.nn-19__tlab:hover {
opacity: 1;
}
.nn-19__tin:focus-visible + .nn-19__tlab {
outline: 2px solid currentColor;
outline-offset: 2px;
}
@media (prefers-color-scheme: light) {
.nn-19:has(#nn-19-theme:checked) {
--nn-19-bg: #080617;
--nn-19-line: #221a44;
--nn-19-ink: #f4eefb;
--nn-19-muted: #a294c4;
--nn-19-neon: #ff5ec4;
}
}
@media (prefers-color-scheme: dark) {
.nn-19:has(#nn-19-theme:checked) {
--nn-19-bg: #eceaf4;
--nn-19-line: #c9c2dc;
--nn-19-ink: #120c22;
--nn-19-muted: #4f4570;
--nn-19-neon: #a3126f;
}
}
.nn-19 {
--nn-19-hue: 330;
--nn-19-cell: 3rem;
--nn-19-dur: 2.4s;
--nn-19-bg: #080617;
--nn-19-line: #221a44;
--nn-19-ink: #f4eefb;
--nn-19-muted: #a294c4;
--nn-19-neon: #ff5ec4;
--nn-19-core: rgba(255, 94, 196, .9);
--nn-19-halo: rgba(255, 94, 196, .4);
--nn-19-font-display: "Avenir Next Condensed", "Roboto Condensed", "Arial Narrow", system-ui, sans-serif;
width: 100%;
min-height: 100vh;
position: relative;
display: block;
box-sizing: border-box;
background: var(--nn-19-bg);
color: var(--nn-19-ink);
font-family: system-ui, sans-serif;
}
.nn-19 *,
.nn-19 *::before,
.nn-19 *::after {
box-sizing: border-box;
}
@supports (color: oklch(50% 0.1 200)) {
.nn-19 {
--nn-19-bg: oklch(13% 0.04 290);
--nn-19-line: oklch(28% 0.06 290);
--nn-19-ink: oklch(96% 0.016 320);
--nn-19-muted: oklch(73% 0.04 300);
--nn-19-neon: oklch(76% 0.21 var(--nn-19-hue));
--nn-19-core: oklch(78% 0.21 var(--nn-19-hue) / .9);
--nn-19-halo: oklch(74% 0.2 var(--nn-19-hue) / .4);
}
}
@media (prefers-color-scheme: light) {
.nn-19 {
--nn-19-bg: #eceaf4;
--nn-19-line: #c9c2dc;
--nn-19-ink: #120c22;
--nn-19-muted: #4f4570;
--nn-19-neon: #a3126f;
}
}
.nn-19[data-theme="light"] {
--nn-19-bg: #eceaf4;
--nn-19-line: #c9c2dc;
--nn-19-ink: #120c22;
--nn-19-muted: #4f4570;
--nn-19-neon: #a3126f;
}
.nn-19[data-theme="dark"] {
--nn-19-bad: #f4a4a4;
--nn-19-warn: #f0c88a;
--nn-19-bg: #080617;
--nn-19-line: #221a44;
--nn-19-ink: #f4eefb;
--nn-19-muted: #a294c4;
--nn-19-neon: #ff5ec4;
}
.nn-19[data-speed="slow"] {
--nn-19-dur: 4.5s;
}
.nn-19[data-speed="fast"] {
--nn-19-dur: 1.2s;
}
.nn-19[data-density="tight"] {
--nn-19-cell: 1.75rem;
}
.nn-19[data-density="wide"] {
--nn-19-cell: 4.5rem;
}
.nn-19__stage {
max-width: 68rem;
margin: 0 auto;
padding: clamp(1.75rem, 5vw, 2.5rem) clamp(1.25rem, 5vw, 2.25rem) clamp(2.25rem, 6vw, 3rem);
display: grid;
gap: clamp(1.5rem, 4vw, 2rem);
}
.nn-19__kicker {
margin: 0 0 .45rem;
font-family: var(--nn-19-font-display);
font-size: .82rem;
letter-spacing: .22em;
text-transform: uppercase;
color: var(--nn-19-neon);
}
.nn-19__heading {
margin: 0;
font-family: var(--nn-19-font-display);
font-size: clamp(1.5rem, 5.5vw, 2.4rem);
font-weight: 600;
text-transform: uppercase;
letter-spacing: .02em;
text-wrap: balance;
}
.nn-19__sub {
margin: .55rem 0 0;
max-width: 42rem;
font-size: .9rem;
line-height: 1.6;
color: var(--nn-19-muted);
text-wrap: pretty;
}
.nn-19__controls {
margin-top: .9rem;
display: flex;
gap: .4rem;
flex-wrap: wrap;
}
.nn-19__btn {
min-height: 44px;
padding: 0 .9rem;
border: 1px solid var(--nn-19-line);
border-radius: .2rem;
background: transparent;
color: var(--nn-19-muted);
font-family: var(--nn-19-font-display);
font-size: .8rem;
letter-spacing: .08em;
text-transform: uppercase;
cursor: pointer;
}
.nn-19__btn:hover {
color: var(--nn-19-ink);
border-color: var(--nn-19-neon);
}
.nn-19__btn[aria-pressed="true"] {
color: var(--nn-19-ink);
border-color: var(--nn-19-neon);
box-shadow: 0 0 .35rem var(--nn-19-halo);
}
.nn-19__btn:focus-visible {
outline: 2px solid var(--nn-19-ink);
outline-offset: 2px;
}
.nn-19__frame {
position: relative;
isolation: isolate;
overflow: hidden;
min-height: clamp(16rem, 48vw, 26rem);
border: 1px solid var(--nn-19-line);
background: linear-gradient(180deg, #0b0824 0%, #080617 48%, #050411 100%);
}
.nn-19__sky {
position: absolute;
inset: 0 0 52% 0;
background: radial-gradient(70% 120% at 50% 100%, color-mix(in srgb, var(--nn-19-neons) 22%, transparent), transparent 70%);
}
.nn-19__horizon {
position: absolute;
left: 0;
right: 0;
top: 48%;
height: 1px;
background: var(--nn-19-neons);
box-shadow: 0 0 .35rem var(--nn-19-core), 0 0 clamp(1.5rem, 7vw, 3rem) var(--nn-19-halo);
}
.nn-19__floor {
position: absolute;
inset: 48% 0 0 0;
perspective: 12rem;
perspective-origin: 50% 0%;
overflow: hidden;
}
.nn-19__plane {
position: absolute;
left: -50%;
right: -50%;
top: 0;
height: 300%;
transform-origin: 50% 0%;
transform: rotateX(74deg);
background-image: repeating-linear-gradient(90deg, var(--nn-19-neons) 0 1px, transparent 1px var(--nn-19-cell)), repeating-linear-gradient(180deg, var(--nn-19-neons) 0 1px, transparent 1px var(--nn-19-cell));
-webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 22%, #000 100%);
mask-image: linear-gradient(180deg, transparent 0%, #000 22%, #000 100%);
animation: nn-19-scroll var(--nn-19-dur) linear infinite;
}
@keyframes nn-19-scroll {
from {
transform: rotateX(74deg) translate3d(0, 0, 0);
}
to {
transform: rotateX(74deg) translate3d(0, var(--nn-19-cell), 0);
}
}
.nn-19__vignette {
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(110% 80% at 50% 48%, transparent 45%, rgba(3, 2, 10, .8) 100%);
}
.nn-19__facts {
margin: 0;
display: grid;
gap: .4rem;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
}
.nn-19__facts div {
display: grid;
gap: .15rem;
padding: .5rem .6rem;
border: 1px solid var(--nn-19-line);
min-inline-size: 0;
}
.nn-19__facts dt {
font-family: var(--nn-19-font-display);
font-size: .68rem;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--nn-19-muted);
}
.nn-19__facts dd {
margin: 0;
font-family: ui-monospace, Menlo, monospace;
font-size: .72rem;
color: var(--nn-19-ink);
}
@media (prefers-reduced-motion: reduce) {
.nn-19__plane {
animation: none;
}
}(() => {
const root = document.querySelector('.nn-19');
if (!root || root.dataset.nn19) return;
root.dataset.nn19 = 'on';
const dens = root.querySelector('#nn-19-density');
const steps = [['normal', '3rem'], ['tight', '1.75rem'], ['wide', '4.5rem']];
let d = 0;
root.addEventListener('click', (e) => {
const speed = e.target.closest('[data-nn19-speed]');
if (speed) {
root.dataset.speed = speed.dataset.nn19Speed;
root.querySelectorAll('[data-nn19-speed]').forEach((b) => b.setAttribute('aria-pressed', String(b === speed)));
return;
}
if (e.target.closest('#nn-19-density')) {
d = (d + 1) % steps.length;
root.dataset.density = steps[d][0];
dens.textContent = 'Density: ' + steps[d][1];
}
});
})();(() => {
const root = document.querySelector('.nn-19');
if (!root || root.dataset.nn19) return;
root.dataset.nn19 = 'on';
const dens = root.querySelector('#nn-19-density');
const steps = [['normal', '3rem'], ['tight', '1.75rem'], ['wide', '4.5rem']];
let d = 0;
root.addEventListener('click', (e) => {
const speed = e.target.closest('[data-nn19-speed]');
if (speed) {
root.dataset.speed = speed.dataset.nn19Speed;
root.querySelectorAll('[data-nn19-speed]').forEach((b) => b.setAttribute('aria-pressed', String(b === speed)));
return;
}
if (e.target.closest('#nn-19-density')) {
d = (d + 1) % steps.length;
root.dataset.density = steps[d][0];
dens.textContent = 'Density: ' + steps[d][1];
}
});
})();Here's a working CSS Neon Effect from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Infinite Neon Grid and Perspective Floor
Source: https://codefronts.com/design-styles/css-neon/infinite-neon-grid/
A perspective floor treated as geometry plus falloff: a repeating grid on a rotated plane, translated by exactly one cell per loop so the motion is seamless, with glow fading toward the horizon via a mask rather than by dimming the lines. Speed and density are adjustable, and the whole thing animates one transform.
## HTML
```html
<section class="nn-19" aria-labelledby="nn-19-heading">
<div class="nn-19__stage">
<div class="nn-19__theme">
<input class="nn-19__tin" type="checkbox" id="nn-19-theme">
<label class="nn-19__tlab" for="nn-19-theme">
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M8 1.5v2M8 12.5v2M1.5 8h2M12.5 8h2M3.4 3.4l1.4 1.4M11.2 11.2l1.4 1.4M12.6 3.4l-1.4 1.4M4.8 11.2l-1.4 1.4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path><circle cx="8" cy="8" r="2.6" fill="none" stroke="currentColor" stroke-width="1.5"></circle></svg>
Flip light / dark
</label>
</div>
<header class="nn-19__head">
<p class="nn-19__kicker">Geometry and falloff</p>
<h2 class="nn-19__heading" id="nn-19-heading">A floor that loops on exactly one cell</h2>
<p class="nn-19__sub">One transform animation, one gradient, one mask. The seam disappears because the loop distance is the grid pitch.</p>
<div class="nn-19__controls">
<button class="nn-19__btn" type="button" data-nn19-speed="slow">Slow</button>
<button class="nn-19__btn" type="button" data-nn19-speed="normal" aria-pressed="true">Normal</button>
<button class="nn-19__btn" type="button" data-nn19-speed="fast">Fast</button>
<button class="nn-19__btn nn-19__btn--alt" type="button" id="nn-19-density">Density: 3rem</button>
</div>
</header>
<div class="nn-19__frame">
<div class="nn-19__sky"></div>
<span class="nn-19__horizon"></span>
<div class="nn-19__floor"><div class="nn-19__plane"></div></div>
<div class="nn-19__vignette"></div>
</div>
<dl class="nn-19__facts">
<div><dt>loop</dt><dd>translate3d(0, one cell, 0)</dd></div>
<div><dt>plane</dt><dd>rotateX(74deg), perspective 12rem</dd></div>
<div><dt>falloff</dt><dd>mask-image fade toward the horizon</dd></div>
<div><dt>animated</dt><dd>transform only</dd></div>
</dl>
</div>
</section>
```
## CSS
```css
.nn-19 {
--nn-19-neons: #ff5ec4;
--nn-19-bad: #f4a4a4;
--nn-19-warn: #f0c88a;
--nn-19-spec-neon: #ff5ec4;
}
@media (prefers-color-scheme: light) {
.nn-19 {
--nn-19-bad: #a12a20;
--nn-19-warn: #78490a;
}
.nn-19:has(#nn-19-theme:checked) {
--nn-19-bad: #f4a4a4;
--nn-19-warn: #f0c88a;
}
}
@media (prefers-color-scheme: dark) {
.nn-19:has(#nn-19-theme:checked) {
--nn-19-bad: #a12a20;
--nn-19-warn: #78490a;
}
}
.nn-19[data-theme="light"] {
--nn-19-bad: #a12a20;
--nn-19-warn: #78490a;
}
.nn-19 {
--nn-19-spec-ink: #f4eefb;
--nn-19-spec-dim: #a294c4;
}
.nn-19__theme {
position: absolute;
inset-block-start: clamp(10px, 2vw, 18px);
inset-inline-end: clamp(10px, 2vw, 18px);
z-index: 5;
display: flex;
justify-content: flex-end;
}
.nn-19__tin {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
}
.nn-19__tlab {
display: inline-flex;
align-items: center;
gap: .4rem;
min-height: 44px;
padding: 0 .8rem;
border: 1px solid currentColor;
font-size: .7rem;
letter-spacing: .12em;
text-transform: uppercase;
cursor: pointer;
opacity: .7;
}
.nn-19__tlab svg {
width: .9rem;
height: .9rem;
flex: none;
}
.nn-19__tlab:hover {
opacity: 1;
}
.nn-19__tin:focus-visible + .nn-19__tlab {
outline: 2px solid currentColor;
outline-offset: 2px;
}
@media (prefers-color-scheme: light) {
.nn-19:has(#nn-19-theme:checked) {
--nn-19-bg: #080617;
--nn-19-line: #221a44;
--nn-19-ink: #f4eefb;
--nn-19-muted: #a294c4;
--nn-19-neon: #ff5ec4;
}
}
@media (prefers-color-scheme: dark) {
.nn-19:has(#nn-19-theme:checked) {
--nn-19-bg: #eceaf4;
--nn-19-line: #c9c2dc;
--nn-19-ink: #120c22;
--nn-19-muted: #4f4570;
--nn-19-neon: #a3126f;
}
}
.nn-19 {
--nn-19-hue: 330;
--nn-19-cell: 3rem;
--nn-19-dur: 2.4s;
--nn-19-bg: #080617;
--nn-19-line: #221a44;
--nn-19-ink: #f4eefb;
--nn-19-muted: #a294c4;
--nn-19-neon: #ff5ec4;
--nn-19-core: rgba(255, 94, 196, .9);
--nn-19-halo: rgba(255, 94, 196, .4);
--nn-19-font-display: "Avenir Next Condensed", "Roboto Condensed", "Arial Narrow", system-ui, sans-serif;
width: 100%;
min-height: 100vh;
position: relative;
display: block;
box-sizing: border-box;
background: var(--nn-19-bg);
color: var(--nn-19-ink);
font-family: system-ui, sans-serif;
}
.nn-19 *,
.nn-19 *::before,
.nn-19 *::after {
box-sizing: border-box;
}
@supports (color: oklch(50% 0.1 200)) {
.nn-19 {
--nn-19-bg: oklch(13% 0.04 290);
--nn-19-line: oklch(28% 0.06 290);
--nn-19-ink: oklch(96% 0.016 320);
--nn-19-muted: oklch(73% 0.04 300);
--nn-19-neon: oklch(76% 0.21 var(--nn-19-hue));
--nn-19-core: oklch(78% 0.21 var(--nn-19-hue) / .9);
--nn-19-halo: oklch(74% 0.2 var(--nn-19-hue) / .4);
}
}
@media (prefers-color-scheme: light) {
.nn-19 {
--nn-19-bg: #eceaf4;
--nn-19-line: #c9c2dc;
--nn-19-ink: #120c22;
--nn-19-muted: #4f4570;
--nn-19-neon: #a3126f;
}
}
.nn-19[data-theme="light"] {
--nn-19-bg: #eceaf4;
--nn-19-line: #c9c2dc;
--nn-19-ink: #120c22;
--nn-19-muted: #4f4570;
--nn-19-neon: #a3126f;
}
.nn-19[data-theme="dark"] {
--nn-19-bad: #f4a4a4;
--nn-19-warn: #f0c88a;
--nn-19-bg: #080617;
--nn-19-line: #221a44;
--nn-19-ink: #f4eefb;
--nn-19-muted: #a294c4;
--nn-19-neon: #ff5ec4;
}
.nn-19[data-speed="slow"] {
--nn-19-dur: 4.5s;
}
.nn-19[data-speed="fast"] {
--nn-19-dur: 1.2s;
}
.nn-19[data-density="tight"] {
--nn-19-cell: 1.75rem;
}
.nn-19[data-density="wide"] {
--nn-19-cell: 4.5rem;
}
.nn-19__stage {
max-width: 68rem;
margin: 0 auto;
padding: clamp(1.75rem, 5vw, 2.5rem) clamp(1.25rem, 5vw, 2.25rem) clamp(2.25rem, 6vw, 3rem);
display: grid;
gap: clamp(1.5rem, 4vw, 2rem);
}
.nn-19__kicker {
margin: 0 0 .45rem;
font-family: var(--nn-19-font-display);
font-size: .82rem;
letter-spacing: .22em;
text-transform: uppercase;
color: var(--nn-19-neon);
}
.nn-19__heading {
margin: 0;
font-family: var(--nn-19-font-display);
font-size: clamp(1.5rem, 5.5vw, 2.4rem);
font-weight: 600;
text-transform: uppercase;
letter-spacing: .02em;
text-wrap: balance;
}
.nn-19__sub {
margin: .55rem 0 0;
max-width: 42rem;
font-size: .9rem;
line-height: 1.6;
color: var(--nn-19-muted);
text-wrap: pretty;
}
.nn-19__controls {
margin-top: .9rem;
display: flex;
gap: .4rem;
flex-wrap: wrap;
}
.nn-19__btn {
min-height: 44px;
padding: 0 .9rem;
border: 1px solid var(--nn-19-line);
border-radius: .2rem;
background: transparent;
color: var(--nn-19-muted);
font-family: var(--nn-19-font-display);
font-size: .8rem;
letter-spacing: .08em;
text-transform: uppercase;
cursor: pointer;
}
.nn-19__btn:hover {
color: var(--nn-19-ink);
border-color: var(--nn-19-neon);
}
.nn-19__btn[aria-pressed="true"] {
color: var(--nn-19-ink);
border-color: var(--nn-19-neon);
box-shadow: 0 0 .35rem var(--nn-19-halo);
}
.nn-19__btn:focus-visible {
outline: 2px solid var(--nn-19-ink);
outline-offset: 2px;
}
.nn-19__frame {
position: relative;
isolation: isolate;
overflow: hidden;
min-height: clamp(16rem, 48vw, 26rem);
border: 1px solid var(--nn-19-line);
background: linear-gradient(180deg, #0b0824 0%, #080617 48%, #050411 100%);
}
.nn-19__sky {
position: absolute;
inset: 0 0 52% 0;
background: radial-gradient(70% 120% at 50% 100%, color-mix(in srgb, var(--nn-19-neons) 22%, transparent), transparent 70%);
}
.nn-19__horizon {
position: absolute;
left: 0;
right: 0;
top: 48%;
height: 1px;
background: var(--nn-19-neons);
box-shadow: 0 0 .35rem var(--nn-19-core), 0 0 clamp(1.5rem, 7vw, 3rem) var(--nn-19-halo);
}
.nn-19__floor {
position: absolute;
inset: 48% 0 0 0;
perspective: 12rem;
perspective-origin: 50% 0%;
overflow: hidden;
}
.nn-19__plane {
position: absolute;
left: -50%;
right: -50%;
top: 0;
height: 300%;
transform-origin: 50% 0%;
transform: rotateX(74deg);
background-image: repeating-linear-gradient(90deg, var(--nn-19-neons) 0 1px, transparent 1px var(--nn-19-cell)), repeating-linear-gradient(180deg, var(--nn-19-neons) 0 1px, transparent 1px var(--nn-19-cell));
-webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 22%, #000 100%);
mask-image: linear-gradient(180deg, transparent 0%, #000 22%, #000 100%);
animation: nn-19-scroll var(--nn-19-dur) linear infinite;
}
@keyframes nn-19-scroll {
from {
transform: rotateX(74deg) translate3d(0, 0, 0);
}
to {
transform: rotateX(74deg) translate3d(0, var(--nn-19-cell), 0);
}
}
.nn-19__vignette {
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(110% 80% at 50% 48%, transparent 45%, rgba(3, 2, 10, .8) 100%);
}
.nn-19__facts {
margin: 0;
display: grid;
gap: .4rem;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
}
.nn-19__facts div {
display: grid;
gap: .15rem;
padding: .5rem .6rem;
border: 1px solid var(--nn-19-line);
min-inline-size: 0;
}
.nn-19__facts dt {
font-family: var(--nn-19-font-display);
font-size: .68rem;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--nn-19-muted);
}
.nn-19__facts dd {
margin: 0;
font-family: ui-monospace, Menlo, monospace;
font-size: .72rem;
color: var(--nn-19-ink);
}
@media (prefers-reduced-motion: reduce) {
.nn-19__plane {
animation: none;
}
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('.nn-19');
if (!root || root.dataset.nn19) return;
root.dataset.nn19 = 'on';
const dens = root.querySelector('#nn-19-density');
const steps = [['normal', '3rem'], ['tight', '1.75rem'], ['wide', '4.5rem']];
let d = 0;
root.addEventListener('click', (e) => {
const speed = e.target.closest('[data-nn19-speed]');
if (speed) {
root.dataset.speed = speed.dataset.nn19Speed;
root.querySelectorAll('[data-nn19-speed]').forEach((b) => b.setAttribute('aria-pressed', String(b === speed)));
return;
}
if (e.target.closest('#nn-19-density')) {
d = (d + 1) % steps.length;
root.dataset.density = steps[d][0];
dens.textContent = 'Density: ' + steps[d][1];
}
});
})();
```Here's a working CSS Neon Effect from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Infinite Neon Grid and Perspective Floor
Source: https://codefronts.com/design-styles/css-neon/infinite-neon-grid/
A perspective floor treated as geometry plus falloff: a repeating grid on a rotated plane, translated by exactly one cell per loop so the motion is seamless, with glow fading toward the horizon via a mask rather than by dimming the lines. Speed and density are adjustable, and the whole thing animates one transform.
## HTML
```html
<section class="nn-19" aria-labelledby="nn-19-heading">
<div class="nn-19__stage">
<div class="nn-19__theme">
<input class="nn-19__tin" type="checkbox" id="nn-19-theme">
<label class="nn-19__tlab" for="nn-19-theme">
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M8 1.5v2M8 12.5v2M1.5 8h2M12.5 8h2M3.4 3.4l1.4 1.4M11.2 11.2l1.4 1.4M12.6 3.4l-1.4 1.4M4.8 11.2l-1.4 1.4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path><circle cx="8" cy="8" r="2.6" fill="none" stroke="currentColor" stroke-width="1.5"></circle></svg>
Flip light / dark
</label>
</div>
<header class="nn-19__head">
<p class="nn-19__kicker">Geometry and falloff</p>
<h2 class="nn-19__heading" id="nn-19-heading">A floor that loops on exactly one cell</h2>
<p class="nn-19__sub">One transform animation, one gradient, one mask. The seam disappears because the loop distance is the grid pitch.</p>
<div class="nn-19__controls">
<button class="nn-19__btn" type="button" data-nn19-speed="slow">Slow</button>
<button class="nn-19__btn" type="button" data-nn19-speed="normal" aria-pressed="true">Normal</button>
<button class="nn-19__btn" type="button" data-nn19-speed="fast">Fast</button>
<button class="nn-19__btn nn-19__btn--alt" type="button" id="nn-19-density">Density: 3rem</button>
</div>
</header>
<div class="nn-19__frame">
<div class="nn-19__sky"></div>
<span class="nn-19__horizon"></span>
<div class="nn-19__floor"><div class="nn-19__plane"></div></div>
<div class="nn-19__vignette"></div>
</div>
<dl class="nn-19__facts">
<div><dt>loop</dt><dd>translate3d(0, one cell, 0)</dd></div>
<div><dt>plane</dt><dd>rotateX(74deg), perspective 12rem</dd></div>
<div><dt>falloff</dt><dd>mask-image fade toward the horizon</dd></div>
<div><dt>animated</dt><dd>transform only</dd></div>
</dl>
</div>
</section>
```
## CSS
```css
.nn-19 {
--nn-19-neons: #ff5ec4;
--nn-19-bad: #f4a4a4;
--nn-19-warn: #f0c88a;
--nn-19-spec-neon: #ff5ec4;
}
@media (prefers-color-scheme: light) {
.nn-19 {
--nn-19-bad: #a12a20;
--nn-19-warn: #78490a;
}
.nn-19:has(#nn-19-theme:checked) {
--nn-19-bad: #f4a4a4;
--nn-19-warn: #f0c88a;
}
}
@media (prefers-color-scheme: dark) {
.nn-19:has(#nn-19-theme:checked) {
--nn-19-bad: #a12a20;
--nn-19-warn: #78490a;
}
}
.nn-19[data-theme="light"] {
--nn-19-bad: #a12a20;
--nn-19-warn: #78490a;
}
.nn-19 {
--nn-19-spec-ink: #f4eefb;
--nn-19-spec-dim: #a294c4;
}
.nn-19__theme {
position: absolute;
inset-block-start: clamp(10px, 2vw, 18px);
inset-inline-end: clamp(10px, 2vw, 18px);
z-index: 5;
display: flex;
justify-content: flex-end;
}
.nn-19__tin {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
}
.nn-19__tlab {
display: inline-flex;
align-items: center;
gap: .4rem;
min-height: 44px;
padding: 0 .8rem;
border: 1px solid currentColor;
font-size: .7rem;
letter-spacing: .12em;
text-transform: uppercase;
cursor: pointer;
opacity: .7;
}
.nn-19__tlab svg {
width: .9rem;
height: .9rem;
flex: none;
}
.nn-19__tlab:hover {
opacity: 1;
}
.nn-19__tin:focus-visible + .nn-19__tlab {
outline: 2px solid currentColor;
outline-offset: 2px;
}
@media (prefers-color-scheme: light) {
.nn-19:has(#nn-19-theme:checked) {
--nn-19-bg: #080617;
--nn-19-line: #221a44;
--nn-19-ink: #f4eefb;
--nn-19-muted: #a294c4;
--nn-19-neon: #ff5ec4;
}
}
@media (prefers-color-scheme: dark) {
.nn-19:has(#nn-19-theme:checked) {
--nn-19-bg: #eceaf4;
--nn-19-line: #c9c2dc;
--nn-19-ink: #120c22;
--nn-19-muted: #4f4570;
--nn-19-neon: #a3126f;
}
}
.nn-19 {
--nn-19-hue: 330;
--nn-19-cell: 3rem;
--nn-19-dur: 2.4s;
--nn-19-bg: #080617;
--nn-19-line: #221a44;
--nn-19-ink: #f4eefb;
--nn-19-muted: #a294c4;
--nn-19-neon: #ff5ec4;
--nn-19-core: rgba(255, 94, 196, .9);
--nn-19-halo: rgba(255, 94, 196, .4);
--nn-19-font-display: "Avenir Next Condensed", "Roboto Condensed", "Arial Narrow", system-ui, sans-serif;
width: 100%;
min-height: 100vh;
position: relative;
display: block;
box-sizing: border-box;
background: var(--nn-19-bg);
color: var(--nn-19-ink);
font-family: system-ui, sans-serif;
}
.nn-19 *,
.nn-19 *::before,
.nn-19 *::after {
box-sizing: border-box;
}
@supports (color: oklch(50% 0.1 200)) {
.nn-19 {
--nn-19-bg: oklch(13% 0.04 290);
--nn-19-line: oklch(28% 0.06 290);
--nn-19-ink: oklch(96% 0.016 320);
--nn-19-muted: oklch(73% 0.04 300);
--nn-19-neon: oklch(76% 0.21 var(--nn-19-hue));
--nn-19-core: oklch(78% 0.21 var(--nn-19-hue) / .9);
--nn-19-halo: oklch(74% 0.2 var(--nn-19-hue) / .4);
}
}
@media (prefers-color-scheme: light) {
.nn-19 {
--nn-19-bg: #eceaf4;
--nn-19-line: #c9c2dc;
--nn-19-ink: #120c22;
--nn-19-muted: #4f4570;
--nn-19-neon: #a3126f;
}
}
.nn-19[data-theme="light"] {
--nn-19-bg: #eceaf4;
--nn-19-line: #c9c2dc;
--nn-19-ink: #120c22;
--nn-19-muted: #4f4570;
--nn-19-neon: #a3126f;
}
.nn-19[data-theme="dark"] {
--nn-19-bad: #f4a4a4;
--nn-19-warn: #f0c88a;
--nn-19-bg: #080617;
--nn-19-line: #221a44;
--nn-19-ink: #f4eefb;
--nn-19-muted: #a294c4;
--nn-19-neon: #ff5ec4;
}
.nn-19[data-speed="slow"] {
--nn-19-dur: 4.5s;
}
.nn-19[data-speed="fast"] {
--nn-19-dur: 1.2s;
}
.nn-19[data-density="tight"] {
--nn-19-cell: 1.75rem;
}
.nn-19[data-density="wide"] {
--nn-19-cell: 4.5rem;
}
.nn-19__stage {
max-width: 68rem;
margin: 0 auto;
padding: clamp(1.75rem, 5vw, 2.5rem) clamp(1.25rem, 5vw, 2.25rem) clamp(2.25rem, 6vw, 3rem);
display: grid;
gap: clamp(1.5rem, 4vw, 2rem);
}
.nn-19__kicker {
margin: 0 0 .45rem;
font-family: var(--nn-19-font-display);
font-size: .82rem;
letter-spacing: .22em;
text-transform: uppercase;
color: var(--nn-19-neon);
}
.nn-19__heading {
margin: 0;
font-family: var(--nn-19-font-display);
font-size: clamp(1.5rem, 5.5vw, 2.4rem);
font-weight: 600;
text-transform: uppercase;
letter-spacing: .02em;
text-wrap: balance;
}
.nn-19__sub {
margin: .55rem 0 0;
max-width: 42rem;
font-size: .9rem;
line-height: 1.6;
color: var(--nn-19-muted);
text-wrap: pretty;
}
.nn-19__controls {
margin-top: .9rem;
display: flex;
gap: .4rem;
flex-wrap: wrap;
}
.nn-19__btn {
min-height: 44px;
padding: 0 .9rem;
border: 1px solid var(--nn-19-line);
border-radius: .2rem;
background: transparent;
color: var(--nn-19-muted);
font-family: var(--nn-19-font-display);
font-size: .8rem;
letter-spacing: .08em;
text-transform: uppercase;
cursor: pointer;
}
.nn-19__btn:hover {
color: var(--nn-19-ink);
border-color: var(--nn-19-neon);
}
.nn-19__btn[aria-pressed="true"] {
color: var(--nn-19-ink);
border-color: var(--nn-19-neon);
box-shadow: 0 0 .35rem var(--nn-19-halo);
}
.nn-19__btn:focus-visible {
outline: 2px solid var(--nn-19-ink);
outline-offset: 2px;
}
.nn-19__frame {
position: relative;
isolation: isolate;
overflow: hidden;
min-height: clamp(16rem, 48vw, 26rem);
border: 1px solid var(--nn-19-line);
background: linear-gradient(180deg, #0b0824 0%, #080617 48%, #050411 100%);
}
.nn-19__sky {
position: absolute;
inset: 0 0 52% 0;
background: radial-gradient(70% 120% at 50% 100%, color-mix(in srgb, var(--nn-19-neons) 22%, transparent), transparent 70%);
}
.nn-19__horizon {
position: absolute;
left: 0;
right: 0;
top: 48%;
height: 1px;
background: var(--nn-19-neons);
box-shadow: 0 0 .35rem var(--nn-19-core), 0 0 clamp(1.5rem, 7vw, 3rem) var(--nn-19-halo);
}
.nn-19__floor {
position: absolute;
inset: 48% 0 0 0;
perspective: 12rem;
perspective-origin: 50% 0%;
overflow: hidden;
}
.nn-19__plane {
position: absolute;
left: -50%;
right: -50%;
top: 0;
height: 300%;
transform-origin: 50% 0%;
transform: rotateX(74deg);
background-image: repeating-linear-gradient(90deg, var(--nn-19-neons) 0 1px, transparent 1px var(--nn-19-cell)), repeating-linear-gradient(180deg, var(--nn-19-neons) 0 1px, transparent 1px var(--nn-19-cell));
-webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 22%, #000 100%);
mask-image: linear-gradient(180deg, transparent 0%, #000 22%, #000 100%);
animation: nn-19-scroll var(--nn-19-dur) linear infinite;
}
@keyframes nn-19-scroll {
from {
transform: rotateX(74deg) translate3d(0, 0, 0);
}
to {
transform: rotateX(74deg) translate3d(0, var(--nn-19-cell), 0);
}
}
.nn-19__vignette {
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(110% 80% at 50% 48%, transparent 45%, rgba(3, 2, 10, .8) 100%);
}
.nn-19__facts {
margin: 0;
display: grid;
gap: .4rem;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
}
.nn-19__facts div {
display: grid;
gap: .15rem;
padding: .5rem .6rem;
border: 1px solid var(--nn-19-line);
min-inline-size: 0;
}
.nn-19__facts dt {
font-family: var(--nn-19-font-display);
font-size: .68rem;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--nn-19-muted);
}
.nn-19__facts dd {
margin: 0;
font-family: ui-monospace, Menlo, monospace;
font-size: .72rem;
color: var(--nn-19-ink);
}
@media (prefers-reduced-motion: reduce) {
.nn-19__plane {
animation: none;
}
}
```
## JavaScript
```js
(() => {
const root = document.querySelector('.nn-19');
if (!root || root.dataset.nn19) return;
root.dataset.nn19 = 'on';
const dens = root.querySelector('#nn-19-density');
const steps = [['normal', '3rem'], ['tight', '1.75rem'], ['wide', '4.5rem']];
let d = 0;
root.addEventListener('click', (e) => {
const speed = e.target.closest('[data-nn19-speed]');
if (speed) {
root.dataset.speed = speed.dataset.nn19Speed;
root.querySelectorAll('[data-nn19-speed]').forEach((b) => b.setAttribute('aria-pressed', String(b === speed)));
return;
}
if (e.target.closest('#nn-19-density')) {
d = (d + 1) % steps.length;
root.dataset.density = steps[d][0];
dens.textContent = 'Density: ' + steps[d][1];
}
});
})();
```How this works
The loop is a distance, not a duration. A scrolling grid only reads as infinite if each cycle moves the plane by exactly one grid cell. Animate translate3d(0, var(--cell), 0) from zero to one cell and the frame at 100% is pixel-identical to the frame at 0%, so there is no seam. Get that distance wrong by a pixel and you get a visible hitch once per cycle, which is the classic tell.
Perspective belongs on the parent, rotation on the child. The container sets perspective and a perspective-origin at the horizon; the plane sets rotateX. Splitting them lets the plane be arbitrarily long without changing the vanishing point, which is what makes the grid appear to continue past the top of the frame rather than ending at it.
Falloff comes from a mask, not from dimming the lines. The grid is one gradient, so per-line brightness is not available. A mask-image that fades to transparent toward the horizon reduces the whole plane's contribution with distance, taking its glow with it. That reads as atmosphere; dimming the entire grid uniformly reads as a lower opacity setting.
Everything moves on the compositor. One transform animation on the plane, no shadow or filter animation anywhere — the rule from demo 07. The speed and density controls write custom properties, and the browser recalculates the gradient once per change rather than per frame. prefers-reduced-motion stops the drift and leaves the floor fully lit.
Make it yours
- Change
--nn-19-cellto retune density; the loop distance follows automatically. - Adjust the plane's
rotateXangle for a shallower or steeper floor. - Move
perspective-originupward to raise the horizon line in the frame. - Retint with
--nn-19-hue; the grid lines and the horizon bar share the token. - Extend the mask's fade stop to push the visible depth further back.
- Add a second plane above the horizon, rotated the other way, for a ceiling.
Gotchas — read before shipping
- A loop distance that is not exactly one cell produces a visible jump once per cycle, however smooth the easing.
- Putting
perspectiveandrotateXon the same element moves the vanishing point with the plane and breaks the illusion. - Dimming the whole grid instead of masking it reads as reduced opacity rather than as distance.
- Animating the gradient's
background-positioninstead of a transform pushes the work back onto paint.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 120+ | 15.4+ | 113+ | 120+ |
Unprefixed mask-image sets the Chrome floor at 120 (it shipped prefixed much earlier, and -webkit-mask-image is declared alongside); oklch() sets Safari 15.4 / Firefox 113. Without mask support the floor still renders and still scrolls seamlessly — the horizon fade is simply absent, so the falloff is carried by the vignette alone. The script-free theme toggle uses :has() (Chrome 105 / Safari 15.4 / Firefox 121) after the @media rules, so without it the demo follows the system colour scheme and the control does nothing.