Skip to content

FreeIn reviewv1.0.0

Grid Horizon

A glowing grid running to a striped sun and a ridge of mountains, banked by the pointer.
npx shadcn@latest add @vibebox/grid-horizon

Controls

Look
1
1
<GridHorizon />

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

  1. 1Add the VibeBox registry to components.json:
    {
      "registries": {
        "@vibebox": "https://motion.vibeboxph.com/r/{name}.json"
      }
    }
  2. 2Add the component with the shadcn CLI:
    npx shadcn@latest add @vibebox/grid-horizon
  3. 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 files
components/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

PropTypeDefaultDescription
childrenReactNode—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).
speednumber1Multiplies the preset's speed; 0 holds the scene still.
intensitynumber1Multiplies the preset's brightness, 0.5 to 1.4.
horizonGnumber0.04Where the horizon sits; 0 is the middle (-0.2 to 0.3; each look sets its own).
sunSizenumber1How large the sun is (0.5 to 1.5; each look sets its own).
cellsnumber1.2How fine the grid is (0.6 to 2.4; each look sets its own).
banknumber1How far the pointer banks the grid (0 to 2; each look sets its own).
interactivebooleantrueThe pointer banks the grid sideways.
labelstring—Accessible name; leave it out when the scene is only a background.
stillbooleanfalseHold one composed frame until false (a gallery card that plays on hover).
classNamestring—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 label it 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 cells under 1.6 or it looks like a screen door.
  • A field of violet halftone dots that swell and shrink in a wave

    A halftone field whose dots swell and shrink on a travelling wave, with a swell under the pointer.

    Backgrounds · WebGL2

  • Green and violet aurora curtains over a mountain ridge and stars

    Aurora

    Free

    Curtains of northern light over a night sky and a mountain ridge, drawn toward the pointer.

    Backgrounds · WebGL2

  • Warm amber bokeh lights of different sizes on a dark background

    Bokeh

    Free

    Out-of-focus lights in three depths, brighter at the rim and drifting with the pointer.

    Backgrounds · WebGL2

↑ ↓ to move↵ to openesc to close