Skip to content
Motion
FreeIn reviewv1.0.0

Spectrum Mesh

A soft spectrum mesh gradient that drifts behind your content and leans toward the cursor; still under reduced motion.

npx shadcn@latest add @vibebox/spectrum-mesh

Controls

0.7
1
<SpectrumMesh />

About

A living background for a hero or call to action. Five soft glows drift on slow, unrelated orbits over the page colour and blend by their weights, so colours meet cleanly instead of turning muddy; near the cursor (or a finger) the mesh bends toward it. It is a fragment shader on the page's shared WebGL2 stage, drawn at about a third of the screen's pixels because soft art upscales without loss, so it is cheap even on large screens. Colours come from your spectrum tokens and the page background, and follow a theme change. A CSS mesh made from the same tokens shows first, and stays on devices without hardware WebGL2. Original to VibeBox Motion.

Use it when

  • A hero, call-to-action band or pricing header wants colour and life behind text.
  • A brand with a gradient identity needs a background that matches it.

Skip it when

  • The section already has a 3D centrepiece or a busy image.
  • Text contrast would suffer: keep content on solid cards or check contrast at the brightest point.

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/spectrum-mesh
  3. 3Use it:
    import { SpectrumMesh } from "@/components/vibebox/spectrum-mesh";
    
    <SpectrumMesh className="rounded-xl border">
      <div className="p-12">
        <h2 className="text-4xl font-semibold">Ready when you are.</h2>
      </div>
    </SpectrumMesh>

Code

3 files
components/vibebox/spectrum-mesh/index.tsx
/**
 * VibeBox Motion · Spectrum Mesh
 * 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 { SpectrumMesh, type SpectrumMeshProps } from "./spectrum-mesh.client";
components/vibebox/spectrum-mesh/spectrum-mesh.client.tsx
/**
 * VibeBox Motion · Spectrum Mesh
 * 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 { ShaderCanvas } from "@/lib/vibebox/shader-canvas";
import { meshPiece, type MeshParams } from "./spectrum-mesh.shader";

/* The CSS mesh: the first paint, the still under no WebGL, and the shape the shader grows from. */
function MeshPoster() {
  return (
    <span
      className="absolute inset-0"
      style={{
        background:
          "radial-gradient(60% 65% at 18% 28%, color-mix(in oklab, var(--spectrum-1) 52%, transparent), transparent 72%), radial-gradient(55% 60% at 82% 22%, color-mix(in oklab, var(--spectrum-5) 46%, transparent), transparent 72%), radial-gradient(65% 70% at 70% 88%, color-mix(in oklab, var(--spectrum-3) 50%, transparent), transparent 72%), radial-gradient(50% 55% at 22% 90%, color-mix(in oklab, var(--spectrum-7) 38%, transparent), transparent 72%), var(--background)",
      }}
    />
  );
}

export type SpectrumMeshProps = {
  /** What sits on the mesh: a heading, a call to action, a whole section. */
  children?: ReactNode;
  /** How strongly the colours show over the background, 0 to 1. */
  intensity?: number;
  /** Drift speed; 0 holds the mesh still. */
  speed?: number;
  /** The mesh leans toward the pointer. */
  interactive?: boolean;
  /** Accessible name; leave it out when the mesh is only a background. */
  label?: string;
  className?: string;
};

const readTokens = (read: (name: string) => [number, number, number]): Partial<MeshParams> => ({
  colors: [read("--spectrum-1"), read("--spectrum-3"), read("--spectrum-4"), read("--spectrum-5"), read("--spectrum-7")],
  base: read("--background"),
});

export function SpectrumMesh({ children, intensity = 0.7, speed = 1, interactive = true, label, className }: SpectrumMeshProps) {
  return (
    <ShaderCanvas
      slug="spectrum-mesh"
      piece={meshPiece}
      params={{ colors: [], base: [0, 0, 0], intensity, speed } satisfies MeshParams}
      readTokens={readTokens}
      scale={0.35}
      poster={<MeshPoster />}
      label={label}
      interactive={interactive}
      className={className}
    >
      {children}
    </ShaderCanvas>
  );
}
components/vibebox/spectrum-mesh/spectrum-mesh.shader.ts
/**
 * VibeBox Motion · Spectrum Mesh
 * 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 type { ShaderPiece } from "@/lib/vibebox/engine/gl";

/*
 * A soft mesh gradient: five glows drift on slow, unrelated orbits over the page background and
 * blend by weight, so colours meet without muddy overlaps. Near the pointer, space bends toward
 * it, so the mesh leans after the cursor. A whisper of grain stops banding on wide screens.
 */

export type MeshParams = {
  colors: readonly (readonly [number, number, number])[];
  base: readonly [number, number, number];
  intensity: number;
  speed: number;
};

const FRAGMENT = /* glsl */ `#version 300 es
precision highp float;
uniform vec2 uResolution;
uniform float uTime;
uniform vec2 uPointer;
uniform float uHover;
uniform vec3 uColors[5];
uniform vec3 uBase;
uniform float uIntensity;
uniform float uSpeed;
out vec4 fragColor;

void main() {
  vec2 uv = gl_FragCoord.xy / uResolution;
  float aspect = uResolution.x / uResolution.y;
  vec2 p = vec2(uv.x * aspect, uv.y);
  vec2 m = vec2(uPointer.x * aspect, uPointer.y);
  vec2 toPointer = p - m;
  p -= toPointer * uHover * 0.22 * exp(-dot(toPointer, toPointer) * 2.6);

  float t = uTime * 0.06 * uSpeed;
  vec3 sum = vec3(0.0);
  float weight = 0.0;
  for (int i = 0; i < 5; i++) {
    float k = float(i);
    vec2 center = vec2(
      aspect * (0.5 + 0.44 * sin(t * (0.83 + k * 0.21) + k * 1.9)),
      0.5 + 0.44 * cos(t * (1.07 + k * 0.17) + k * 2.6)
    );
    float radius = 0.42 + 0.12 * sin(t * 0.61 + k * 1.3);
    vec2 d = p - center;
    float w = exp(-dot(d, d) / (radius * radius));
    sum += uColors[i] * w;
    weight += w;
  }
  vec3 mesh = sum / max(weight, 0.0001);
  vec3 color = mix(uBase, mesh, clamp(weight, 0.0, 1.0) * uIntensity);
  float grain = fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453);
  color += (grain - 0.5) * (2.0 / 255.0);
  fragColor = vec4(color, 1.0);
}
`;

export const meshPiece: ShaderPiece<MeshParams> = {
  key: "vibebox-spectrum-mesh",
  fragment: FRAGMENT,
  animated: true,
  apply(gl, locate, _frame, params) {
    const colors = Array.from({ length: 5 }, (_, index) => params.colors[index % Math.max(1, params.colors.length)] ?? params.base).flat();
    gl.uniform3fv(locate("uColors"), colors);
    gl.uniform3f(locate("uBase"), params.base[0], params.base[1], params.base[2]);
    gl.uniform1f(locate("uIntensity"), params.intensity);
    gl.uniform1f(locate("uSpeed"), params.speed);
  },
};

Props

PropTypeDefaultDescription
childrenReactNode—What sits on the mesh: a heading, a call to action, a whole section.
intensitynumber0.7How strongly the colours show over the background, 0 to 1.
speednumber1Drift speed; 0 holds the mesh still.
interactivebooleantrueThe mesh leans toward the pointer.
labelstring—Accessible name; leave it out when the mesh is only a background.
classNamestring—Merged with cn(); give it a size, radius and border.

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 on it leans the mesh like the cursor does; scrolling is never blocked.
Reduced motion
One still frame of the mesh; no drift or pointer lean.

Edge cases and limits

  • Very light spectrum tokens on a light background give a faint mesh; raise intensity.
  • The mesh fills its box; set a minimum height on the content inside.
  • Spectrum shapes seen through vertical reeded glass

    Reeded-glass refraction over an image or spectrum art; the flutes slide with scroll and lean with the pointer.

    Backgrounds · WebGL2

  • Rippling caustic light over blue water behind a resort heading

    Sunlight through moving water, over any section: caustic light that wanders and ripples under the pointer.

    Backgrounds · WebGL2

  • A ring of light bars pulsing around a floating glass box

    A crown of light bars pulsing like a visualizer around a floating glass box; bars rise toward the pointer.

    3D · three.js

↑ ↓ to move↵ to openesc to close