About
A cellular pattern gives the facets; each cell's id gives its tilt. The colour behind is a three-hue field; every facet samples it at the pixel displaced by its tilt, with the three channels displaced by 1.15, 1.0 and 0.85 of that amount, which is dispersion. A thin bright line where two cells meet is the edge, strongest near the light, and each facet is lit by how its tilt faces a fixed direction.
Use it when
- A software, fintech or AI brand wants a crisp, premium surface.
- A property or architecture studio wants glass.
- A jewellery or beauty brand wants crystal.
Skip it when
- The brand is warm and organic.
- Text must sit directly over many bright edges.
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/prism - 3Use it:
import { Prism } from "@/components/vibebox/prism"; <Prism preset="spectrum" className="min-h-[560px] rounded-xl"> <div className="p-12 text-white"> <h1 className="text-4xl font-semibold">Clear, sharp, finished.</h1> </div> </Prism>
Code
3 filescomponents/vibebox/prism/index.tsx
/**
* VibeBox Motion · Prism
* 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 { Prism, type PrismProps, type PrismColor, type PrismValue } from "./prism.client";
export { PRESET_NAMES as PRISM_PRESETS, presets as prismPresets, type PrismPreset } from "./prism.shader";components/vibebox/prism/prism.client.tsx
/**
* VibeBox Motion · Prism
* 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 { prismPiece, presets, type PrismPreset } from "./prism.shader";
export type PrismColor = "baseP" | "hueA" | "hueB" | "hueC";
export type PrismValue = "facets" | "spread" | "shine";
export type PrismProps = {
/** What sits on the scene: a heading, a call to action, a whole section. */
children?: ReactNode;
/** A named look: `spectrum`, `ice`, `steel`, `quartz`, `gold`. */
preset?: PrismPreset;
/** Overrides single palette colours with any CSS colour. */
colors?: Partial<Record<PrismColor, 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<PrismValue, number>>;
const POSTER: PosterSpec = { type: "linear", from: "baseP", to: "hueB", angle: 135, blobs: [{ color: "hueA", at: [25, 30], size: 38 }, { color: "hueC", at: [75, 70], size: 38 }] };
export function Prism({ children, preset, colors, speed, intensity, interactive = true, label, still, className, facets, spread, shine }: PrismProps) {
return (
<BackdropCanvas
slug="prism"
piece={prismPiece}
presets={presets}
fallback="spectrum"
preset={preset}
colors={colors}
values={{ facets, spread, shine }}
speed={speed}
intensity={intensity}
poster={POSTER}
scale={0.5}
stillTime={3}
still={still}
interactive={interactive}
label={label}
className={className}
>
{children}
</BackdropCanvas>
);
}components/vibebox/prism/prism.shader.ts
/**
* VibeBox Motion · Prism
* 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";
/*
* Cut glass: the plane is divided into facets, each tilted a little differently, and each facet
* shows the colour behind it displaced by its tilt, with red, green and blue displaced by slightly
* different amounts (dispersion), so a facet's edge carries the fringe of a real prism. The facets
* drift and the pointer is a light that glints along their edges.
*/
const SCENE = /* glsl */ `
uniform vec3 uBaseP;
uniform vec3 uHueA;
uniform vec3 uHueB;
uniform vec3 uHueC;
uniform float uFacets;
uniform float uSpread;
uniform float uShine;
uniform float uSpeed;
uniform float uIntensity;
float T;
vec3 field(vec2 q) {
float k = 0.5 + 0.5 * sin(q.x * 1.3 + q.y * 0.7 + T * 0.1);
float m = 0.5 + 0.5 * sin(q.y * 1.6 - q.x * 0.5 - T * 0.08 + 2.0);
vec3 c = mix(lin(uHueA), lin(uHueB), k);
c = mix(c, lin(uHueC), m * 0.7);
return mix(lin(uBaseP), c, 0.62);
}
void main() {
vec2 uv = centred(gl_FragCoord.xy);
T = uTime * uSpeed;
float aspect = uResolution.x / uResolution.y;
vec2 pu = vec2((uPointer.x - 0.5) * aspect, uPointer.y - 0.5);
vec2 p = uv * uFacets + vec2(T * 0.03, T * 0.015);
vec3 v = voronoi(p);
// Each facet's tilt, from its id.
float id = v.z;
vec2 tilt = vec2(h1(int(id * 9731.0)) - 0.5, h1(int(id * 5113.0) + 17) - 0.5) * 2.0;
// Dispersion: the three channels look through the facet at slightly different angles.
vec2 off = tilt * uSpread;
vec3 col;
col.r = field(uv * 2.0 + off * 1.15).r;
col.g = field(uv * 2.0 + off).g;
col.b = field(uv * 2.0 + off * 0.85).b;
// Facet edges: thin bright lines where one facet meets the next, strongest toward the light.
float edge = 1.0 - smoothstep(0.0, 0.07, v.y - v.x);
float toLight = exp(-length(uv - mix(vec2(0.3, 0.2), pu, uHover)) * 1.6);
col += lin(uHueA) * edge * (0.12 + 0.55 * toLight) * uShine;
// Each facet catches the light a little differently.
col *= 0.4 + 0.9 * (0.5 + 0.5 * dot(normalize(tilt + 0.001), vec2(0.6, 0.8))) * (0.45 + toLight);
col *= uIntensity;
col *= 1.0 - 0.3 * smoothstep(0.55, 1.2, length(uv * vec2(0.85, 1.1)));
fragColor = vec4(finish(col, gl_FragCoord.xy), 1.0);
}
`;
export const fragment = compose(SCENE);
export const PRESET_NAMES = ["spectrum", "ice", "steel", "quartz", "gold"] as const;
export type PrismPreset = (typeof PRESET_NAMES)[number];
export const presets: Record<PrismPreset, ScenePreset> = {
spectrum: {
colors: { baseP: "#0a0c1c", hueA: "#36d6ff", hueB: "#7a4cff", hueC: "#ff4fb6" },
values: { facets: 3.2, spread: 0.6, shine: 1, speed: 0.7, intensity: 1.05 },
},
ice: {
colors: { baseP: "#06182a", hueA: "#bfe9ff", hueB: "#4aa8ff", hueC: "#e9fbff" },
values: { facets: 3.6, spread: 0.5, shine: 1.1, speed: 0.6, intensity: 1.05 },
},
steel: {
colors: { baseP: "#0c0e12", hueA: "#cbd5e1", hueB: "#6b7686", hueC: "#f2f5f9" },
values: { facets: 3, spread: 0.45, shine: 1, speed: 0.5, intensity: 1.05 },
},
quartz: {
colors: { baseP: "#160a14", hueA: "#ffc2da", hueB: "#c8a2ff", hueC: "#ffe3ec" },
values: { facets: 3.4, spread: 0.55, shine: 1, speed: 0.55, intensity: 1.05 },
},
gold: {
colors: { baseP: "#140d05", hueA: "#ffd27a", hueB: "#c98a2b", hueC: "#fff0c9" },
values: { facets: 3.2, spread: 0.5, shine: 1.1, speed: 0.55, intensity: 1.05 },
},
};
export const prismPiece = createScenePiece("prism", fragment);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | What sits on the scene: a heading, a call to action, a whole section. |
| preset | "spectrum" | "ice" | "steel" | "quartz" | "gold" | "spectrum" | The look: spectrum, ice, steel, quartz, gold. |
| colors | { baseP?: string; hueA?: string; hueB?: string; hueC?: string } | — | Overrides single palette colours with any CSS colour: baseP (the dark behind the glass), hueA (the first colour in the glass), hueB (the second colour in the glass), hueC (the third colour in the glass). |
| 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. |
| facets | number | 3.2 | How many facets cross the frame (1.5 to 7; each look sets its own). |
| spread | number | 0.6 | How far each facet bends the colour (0.1 to 1.4; each look sets its own). |
| shine | number | 1 | How bright the facet edges glint (0 to 2; each look sets its own). |
| interactive | boolean | true | The pointer is a light that glints along the facet edges. |
| 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 glass; the facets do not drift.
Edge cases and limits
- Edges are bright: keep small text off them.
- Steel and quartz are pale; use dark text.
- Few facets (under 2) look like a gradient; use 3 to 4.
Related

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

City Night
FreeA skyline of lit windows against a glowing sky, with a road of light trails in front.
Backgrounds · WebGL2