About
Below the horizon, depth is the inverse of the distance under it, so one division gives both the x and the depth coordinates of a plane; lines are found with the fract of those and widened by their screen-space derivative so they stay a pixel or two wide, and fade into a glow where they would be thinner than a pixel. Above, the sun is a disc with slats cut from its lower half, and the ridge is ridged noise with a bright rim line.
Use it when
- A developer tool, game, crypto, retro or music brand wants its shorthand.
- An arcade, event or club page wants neon.
- A product launch wants a stage.
Skip it when
- The brand is premium and calm; this is loud.
- The audience is not tech or youth.
Install
- 1Add the VibeBox registry to
components.json:{ "registries": { "@vibebox": "https://motion.vibeboxph.com/r/{name}.json" } } - 2Add the component with the shadcn CLI:
npx shadcn@latest add @vibebox/grid-horizon - 3Use it:
import { GridHorizon } from "@/components/vibebox/grid-horizon"; <GridHorizon preset="synthwave" className="min-h-[560px] rounded-xl"> <div className="p-12 text-white"> <h1 className="text-4xl font-semibold">Ship at the speed of light.</h1> </div> </GridHorizon>
Code
3 filescomponents/vibebox/grid-horizon/grid-horizon.client.tsx
/**
* VibeBox Motion · Grid Horizon
* Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
* https://motion.vibeboxph.com/license
* Use it in unlimited personal, commercial and client projects.
* Do not sell or redistribute it as a component, kit, template or builder.
*/
"use client";
import type { ReactNode } from "react";
import { BackdropCanvas, type PosterSpec } from "@/lib/vibebox/backdrop-canvas";
import { gridHorizonPiece, presets, type GridHorizonPreset } from "./grid-horizon.shader";
export type GridHorizonColor = "skyTop" | "skyLow" | "sunA" | "sunB" | "grid" | "ground";
export type GridHorizonValue = "horizonG" | "sunSize" | "cells" | "bank";
export type GridHorizonProps = {
/** What sits on the scene: a heading, a call to action, a whole section. */
children?: ReactNode;
/** A named look: `synthwave`, `arcade`, `ice`, `ember`. */
preset?: GridHorizonPreset;
/** Overrides single palette colours with any CSS colour. */
colors?: Partial<Record<GridHorizonColor, string>>;
/** Multiplies the preset's speed (1 keeps it, 0 holds the scene still). */
speed?: number;
/** Multiplies the preset's brightness. */
intensity?: number;
/** The pointer and touch move the scene. */
interactive?: boolean;
/** Accessible name; leave it out when the scene is only a background. */
label?: string;
/** Hold one still frame until this is false (for a gallery card). */
still?: boolean;
className?: string;
} & Partial<Record<GridHorizonValue, number>>;
const POSTER: PosterSpec = { type: "horizon", top: "skyTop", horizon: "skyLow", groundTop: "ground", groundBottom: "ground", at: 52, glow: "sunA", glowAt: [50, 48] };
export function GridHorizon({ children, preset, colors, speed, intensity, interactive = true, label, still, className, horizonG, sunSize, cells, bank }: GridHorizonProps) {
return (
<BackdropCanvas
slug="grid-horizon"
piece={gridHorizonPiece}
presets={presets}
fallback="synthwave"
preset={preset}
colors={colors}
values={{ horizonG, sunSize, cells, bank }}
speed={speed}
intensity={intensity}
poster={POSTER}
scale={0.5}
stillTime={2.4}
still={still}
interactive={interactive}
label={label}
className={className}
>
{children}
</BackdropCanvas>
);
}components/vibebox/grid-horizon/grid-horizon.shader.ts
/**
* VibeBox Motion · Grid Horizon
* Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
* https://motion.vibeboxph.com/license
* Use it in unlimited personal, commercial and client projects.
* Do not sell or redistribute it as a component, kit, template or builder.
*/
import { compose, createScenePiece, type ScenePreset } from "@/lib/vibebox/engine/backdrops";
/*
* A plane of light running to a horizon: a perspective grid that scrolls toward the viewer under
* a striped sun and a row of faceted mountains. Line width is kept constant on screen, so the
* far grid fades into a glow instead of shimmering. The pointer banks the grid sideways.
*/
const SCENE = /* glsl */ `
uniform vec3 uSkyTop;
uniform vec3 uSkyLow;
uniform vec3 uSunA;
uniform vec3 uSunB;
uniform vec3 uGrid;
uniform vec3 uGround;
uniform float uHorizonG;
uniform float uSunSize;
uniform float uCells;
uniform float uBank;
uniform float uSpeed;
uniform float uIntensity;
float T;
float mountains(float x) {
float h = ridged(vec2(x * 1.7 + 4.0, 1.0)) * 0.12 + ridged(vec2(x * 4.3, 7.0)) * 0.03;
return h;
}
void main() {
vec2 uv = centred(gl_FragCoord.xy);
T = uTime * uSpeed;
float aspect = uResolution.x / uResolution.y;
float hy = uHorizonG;
float bank = (uPointer.x - 0.5) * uBank * (0.3 + uHover * 0.7);
vec3 col;
if (uv.y > hy) {
// Sky: a gradient, the sun with its slats, and a haze at the horizon.
float t = clamp((uv.y - hy) / 0.6, 0.0, 1.0);
col = lin(mix(uSkyLow, uSkyTop, pow(t, 0.7)));
vec2 sc = vec2(0.0, hy + 0.17 * uSunSize + 0.02);
float sr = 0.2 * uSunSize;
float d = length((uv - sc) * vec2(1.0, 1.0));
float disc = smoothstep(sr + 0.004, sr - 0.004, d);
// Slats: bands cut out of the lower half, wider toward the bottom.
float local = (sc.y - uv.y) / sr; // 0 at the middle, 1 at the bottom of the disc
float cut = local > 0.0 ? smoothstep(0.5, 0.52, fract(local * 7.0 - T * 0.05) ) * smoothstep(0.0, 0.35, local) : 0.0;
disc *= 1.0 - cut * 0.92;
vec3 sun = mix(lin(uSunA), lin(uSunB), clamp(local * 0.5 + 0.5, 0.0, 1.0));
col = mix(col, sun * 1.4, disc);
col += lin(uSunA) * exp(-max(d - sr, 0.0) * 5.0) * 0.28;
// Mountains: a dark silhouette with a bright rim.
float m = hy + mountains(uv.x + bank * 0.2);
float mi = smoothstep(m + 0.003, m - 0.003, uv.y);
float rim = smoothstep(0.012, 0.0, abs(uv.y - m)) ;
col = mix(col, lin(uGround) * 0.9, mi);
col += lin(uGrid) * rim * 0.8;
} else {
// The plane: depth runs as 1 / distance below the horizon.
float dy = hy - uv.y;
float z = 0.18 / max(dy, 0.002);
float k = uCells * 6.0;
float x = (uv.x - bank * dy * 2.0) * z * k;
float zz = z * k * 0.5 + T * 1.2;
vec2 g = vec2(x, zz);
vec2 w = fwidth(g);
vec2 l = abs(fract(g - 0.5) - 0.5) / max(w, 1e-4);
float line = 1.0 - clamp(min(l.x, l.y) - 0.5, 0.0, 1.0);
// Far away the lines are narrower than a pixel; let them melt into a glow instead.
float far = smoothstep(0.0, 0.1, dy);
vec3 ground = lin(uGround);
col = ground * (0.7 + 0.5 * smoothstep(0.35, 0.0, dy));
col += lin(uGrid) * line * far * 1.1;
col += lin(uGrid) * exp(-dy * 22.0) * 0.55;
col += lin(uSunA) * exp(-dy * 9.0) * 0.12;
}
col *= uIntensity;
col *= 1.0 - 0.3 * smoothstep(0.55, 1.2, length(uv * vec2(0.8, 1.1)));
fragColor = vec4(finish(col, gl_FragCoord.xy), 1.0);
}
`;
export const fragment = compose(SCENE);
export const PRESET_NAMES = ["synthwave", "arcade", "ice", "ember"] as const;
export type GridHorizonPreset = (typeof PRESET_NAMES)[number];
export const presets: Record<GridHorizonPreset, ScenePreset> = {
synthwave: {
colors: { skyTop: "#0c0624", skyLow: "#6a1b9a", sunA: "#ff3fa4", sunB: "#ffd23f", grid: "#38d4ff", ground: "#0a0420" },
values: { horizonG: 0.04, sunSize: 1, cells: 1.2, bank: 1, speed: 0.9, intensity: 1.05 },
},
arcade: {
colors: { skyTop: "#050818", skyLow: "#1b2a6a", sunA: "#35d8ff", sunB: "#ff4fb6", grid: "#ff4fb6", ground: "#04061a" },
values: { horizonG: 0.02, sunSize: 0.9, cells: 1.4, bank: 1, speed: 1.1, intensity: 1.05 },
},
ice: {
colors: { skyTop: "#0a1e3a", skyLow: "#7fc6ff", sunA: "#ffffff", sunB: "#bfe9ff", grid: "#bfe9ff", ground: "#06121f" },
values: { horizonG: 0.05, sunSize: 0.8, cells: 1.1, bank: 0.8, speed: 0.7, intensity: 1.05 },
},
ember: {
colors: { skyTop: "#14060a", skyLow: "#8a1f2a", sunA: "#ff7a2a", sunB: "#ffd36a", grid: "#ff7a2a", ground: "#0c0406" },
values: { horizonG: 0.04, sunSize: 1.1, cells: 1.2, bank: 1, speed: 0.8, intensity: 1.05 },
},
};
export const gridHorizonPiece = createScenePiece("grid-horizon", fragment);components/vibebox/grid-horizon/index.tsx
/**
* VibeBox Motion · Grid Horizon
* Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
* https://motion.vibeboxph.com/license
* Use it in unlimited personal, commercial and client projects.
* Do not sell or redistribute it as a component, kit, template or builder.
*/
export { GridHorizon, type GridHorizonProps, type GridHorizonColor, type GridHorizonValue } from "./grid-horizon.client";
export { PRESET_NAMES as GRID_HORIZON_PRESETS, presets as gridHorizonPresets, type GridHorizonPreset } from "./grid-horizon.shader";Props
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | What sits on the scene: a heading, a call to action, a whole section. |
| preset | "synthwave" | "arcade" | "ice" | "ember" | "synthwave" | The look: synthwave, arcade, ice, ember. |
| colors | { skyTop?: string; skyLow?: string; sunA?: string; sunB?: string; grid?: string; ground?: string } | — | Overrides single palette colours with any CSS colour: skyTop (the sky overhead), skyLow (the sky at the horizon), sunA (the sun's upper colour and glow), sunB (the sun's lower colour), grid (the grid lines and the mountain rim), ground (the plane between the lines). |
| speed | number | 1 | Multiplies the preset's speed; 0 holds the scene still. |
| intensity | number | 1 | Multiplies the preset's brightness, 0.5 to 1.4. |
| horizonG | number | 0.04 | Where the horizon sits; 0 is the middle (-0.2 to 0.3; each look sets its own). |
| sunSize | number | 1 | How large the sun is (0.5 to 1.5; each look sets its own). |
| cells | number | 1.2 | How fine the grid is (0.6 to 2.4; each look sets its own). |
| bank | number | 1 | How far the pointer banks the grid (0 to 2; each look sets its own). |
| interactive | boolean | true | The pointer banks the grid sideways. |
| label | string | — | Accessible name; leave it out when the scene is only a background. |
| still | boolean | false | Hold one composed frame until false (a gallery card that plays on hover). |
| className | string | — | Merged with cn(); give it a size and a radius. |
Accessibility and motion
- Keyboard
- Not focusable; content on it keeps its own focus order.
- Screen readers
- Decorative by default; with
labelit is an image with that name. - Touch
- A finger moves the scene like the cursor does; scrolling is never blocked.
- Reduced motion
- One still frame of the horizon; the grid does not scroll.
Edge cases and limits
- The sun is the brightest thing: put copy above or beside it.
- Ice is the lightest look: check white text.
- The grid on phones should stay at
cellsunder 1.6 or it looks like a screen door.
Related

Dot Waves
FreeA halftone field whose dots swell and shrink on a travelling wave, with a swell under the pointer.
Backgrounds · WebGL2

Aurora
FreeCurtains of northern light over a night sky and a mountain ridge, drawn toward the pointer.
Backgrounds · WebGL2

Bokeh
FreeOut-of-focus lights in three depths, brighter at the rim and drifting with the pointer.
Backgrounds · WebGL2