About
A source above the frame and the angle of each pixel from it. Three octaves of one-dimensional value noise on that angle, at three widths and drift speeds, give the shafts; each is raised to a power for contrast and faded with distance from the source. The haze is a gradient toward the source, the bloom two exponentials, the dust a hash grid of small points that twinkle and are brightest where a shaft passes. Slats are a soft band function of the screen's vertical axis, tilted a little, applied to the beams only.
Use it when
- A venue, hotel, cinema, church or gallery wants the feeling of light arriving.
- A wellness or morning brand wants a gentle sunrise behind the headline.
- A product launch wants a stage-light hero without a video.
Skip it when
- The brand is flat and graphic; use Dot Waves or Studio.
- The page needs the beams to hold still behind a lot of text; use
still.
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/rays - 3Use it:
import { Rays } from "@/components/vibebox/rays"; <Rays preset="cathedral" className="min-h-[560px] rounded-xl"> <div className="p-12 text-white"> <h1 className="text-4xl font-semibold">A room that holds the day.</h1> </div> </Rays>
Code
3 filescomponents/vibebox/rays/index.tsx
/**
* VibeBox Motion · Rays
* 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 { Rays, type RaysProps, type RaysColor, type RaysValue } from "./rays.client";
export { PRESET_NAMES as RAYS_PRESETS, presets as raysPresets, type RaysPreset } from "./rays.shader";components/vibebox/rays/rays.client.tsx
/**
* VibeBox Motion · Rays
* 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 { raysPiece, presets, type RaysPreset } from "./rays.shader";
export type RaysColor = "base" | "haze" | "beam" | "core";
export type RaysValue = "sourceX" | "sourceY" | "spread" | "sharp" | "dust" | "blinds";
export type RaysProps = {
/** What sits on the scene: a heading, a call to action, a whole section. */
children?: ReactNode;
/** A named look: `cathedral`, `cinema`, `dawn`, `forest`, `violet`, `mono`. */
preset?: RaysPreset;
/** Overrides single palette colours with any CSS colour. */
colors?: Partial<Record<RaysColor, 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<RaysValue, number>>;
const POSTER: PosterSpec = { type: "radial", base: "base", glow: "beam", edge: "haze", at: [40, 0] };
export function Rays({ children, preset, colors, speed, intensity, interactive = true, label, still, className, sourceX, sourceY, spread, sharp, dust, blinds }: RaysProps) {
return (
<BackdropCanvas
slug="rays"
piece={raysPiece}
presets={presets}
fallback="cathedral"
preset={preset}
colors={colors}
values={{ sourceX, sourceY, spread, sharp, dust, blinds }}
speed={speed}
intensity={intensity}
poster={POSTER}
scale={0.5}
stillTime={3.2}
still={still}
interactive={interactive}
label={label}
className={className}
>
{children}
</BackdropCanvas>
);
}components/vibebox/rays/rays.shader.ts
/**
* VibeBox Motion · Rays
* 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";
/*
* Light through haze: a source above the frame and shafts that fan from it. Three layers of
* angular noise at different widths and drift speeds give the shafts their depth (the broad ones
* move slowly, the fine ones faster), each fading with distance from the source; a bloom sits at
* the source, dust catches the light inside the beams, and the floor of the frame falls into the
* base colour. The pointer swings the source a little.
*/
const SCENE = /* glsl */ `
uniform vec3 uBase;
uniform vec3 uHaze;
uniform vec3 uBeam;
uniform vec3 uCore;
uniform float uSourceX;
uniform float uSourceY;
uniform float uSpread;
uniform float uSharp;
uniform float uDust;
uniform float uBlinds;
uniform float uSpeed;
uniform float uIntensity;
float T;
float ang1(float a, float seed) {
// 1D value noise on the angle, wrapped smoothly.
float i = floor(a);
float f = fract(a);
f = f * f * (3.0 - 2.0 * f);
return mix(h1(int(i) + int(seed * 31.0)), h1(int(i) + 1 + int(seed * 31.0)), f);
}
void main() {
vec2 uv = centred(gl_FragCoord.xy);
T = uTime * uSpeed;
float aspect = uResolution.x / uResolution.y;
vec2 src = vec2(uSourceX * 0.5 * aspect + (uPointer.x - 0.5) * 0.18 * (0.3 + uHover), 0.5 + uSourceY * 0.5);
vec2 v = uv - src;
float dist = length(v);
float a = atan(v.x, -v.y);
// Three layers of shafts.
float s = 0.0;
float w1 = ang1(a * 5.0 * uSpread + T * 0.06, 1.0);
float w2 = ang1(a * 11.0 * uSpread - T * 0.09, 2.0);
float w3 = ang1(a * 27.0 * uSpread + T * 0.14, 3.0);
s += pow(w1, uSharp) * 0.9;
s += pow(w2, uSharp * 1.2) * 0.7;
s += pow(w3, uSharp * 1.6) * 0.45;
// Optional blinds: the shafts are cut into soft bands, as through slats, slightly tilted.
float slats = mix(1.0, smoothstep(0.12, 0.62, abs(fract(uv.y * 5.2 + uv.x * 0.22) - 0.5) * 2.0), uBlinds);
s *= slats;
float reach = exp(-dist * 1.35);
float beam = s * reach;
vec3 col = lin(uBase);
// The haze fills the frame with a soft gradient toward the source.
col += lin(uHaze) * (0.35 * exp(-dist * 1.9) + 0.08);
col += lin(uBeam) * beam * 1.15;
// A bloom at the source.
col += lin(uCore) * (exp(-dist * 6.0) * 0.9 + exp(-dist * 22.0) * 1.4);
// Dust catching the light inside the shafts.
if (uDust > 0.01) {
vec2 g = uv * vec2(34.0, 22.0) + vec2(T * 0.06, -T * 0.11);
vec2 cell = floor(g);
float rnd = h2(ivec2(cell));
vec2 o = h22(ivec2(cell)) - 0.5;
float d = length(fract(g) - 0.5 - o * 0.7);
float tw = 0.5 + 0.5 * sin(T * (0.6 + rnd * 1.6) + rnd * 40.0);
float mote = smoothstep(0.1, 0.0, d) * step(0.78, rnd) * tw;
col += lin(uBeam) * mote * uDust * (0.2 + 1.6 * beam);
}
col *= uIntensity;
col *= 1.0 - 0.4 * smoothstep(0.4, 1.25, length(uv * vec2(0.8, 1.0)));
fragColor = vec4(finish(col, gl_FragCoord.xy), 1.0);
}
`;
export const fragment = compose(SCENE);
export const PRESET_NAMES = ["cathedral", "cinema", "dawn", "forest", "violet", "mono"] as const;
export type RaysPreset = (typeof PRESET_NAMES)[number];
export const presets: Record<RaysPreset, ScenePreset> = {
cathedral: {
colors: { base: "#0b0704", haze: "#5a3410", beam: "#ffcf8a", core: "#fff3d6" },
values: { sourceX: 0.35, sourceY: 0.25, spread: 1, sharp: 2.2, dust: 1, blinds: 0, speed: 0.7, intensity: 1 },
},
cinema: {
colors: { base: "#03060d", haze: "#0f2a4a", beam: "#8ec9ff", core: "#eaf5ff" },
values: { sourceX: -0.3, sourceY: 0.3, spread: 1.1, sharp: 2.6, dust: 0.8, blinds: 0.7, speed: 0.6, intensity: 1 },
},
dawn: {
colors: { base: "#120a14", haze: "#6a2a4a", beam: "#ffb09a", core: "#fff0e0" },
values: { sourceX: 0.0, sourceY: 0.15, spread: 0.9, sharp: 1.8, dust: 0.9, blinds: 0, speed: 0.55, intensity: 1.05 },
},
forest: {
colors: { base: "#030b07", haze: "#0e3a22", beam: "#c9ffb0", core: "#f4ffe0" },
values: { sourceX: 0.45, sourceY: 0.3, spread: 1.2, sharp: 2.4, dust: 1.2, blinds: 0, speed: 0.6, intensity: 1 },
},
violet: {
colors: { base: "#07030f", haze: "#2a1054", beam: "#c79bff", core: "#f4ecff" },
values: { sourceX: -0.15, sourceY: 0.25, spread: 1, sharp: 2, dust: 0.8, blinds: 0.35, speed: 0.65, intensity: 1 },
},
mono: {
colors: { base: "#040405", haze: "#202228", beam: "#ffffff", core: "#ffffff" },
values: { sourceX: 0.2, sourceY: 0.3, spread: 1, sharp: 2.8, dust: 0.6, blinds: 0, speed: 0.5, intensity: 1 },
},
};
export const raysPiece = createScenePiece("rays", fragment);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | What sits on the scene: a heading, a call to action, a whole section. |
| preset | "cathedral" | "cinema" | "dawn" | "forest" | "violet" | "mono" | "cathedral" | The look: cathedral, cinema, dawn, forest, violet, mono. |
| colors | { base?: string; haze?: string; beam?: string; core?: string } | — | Overrides single palette colours with any CSS colour: base (the dark between the beams), haze (the haze that fills the frame near the source), beam (the colour of the shafts and the dust), core (the bloom at the light's source). |
| 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. |
| sourceX | number | 0.35 | Where the light sits across the top, from left (-1) to right (1) (-1 to 1; each look sets its own). |
| sourceY | number | 0.25 | How far the source is above the frame (-0.2 to 1; each look sets its own). |
| spread | number | 1 | How many shafts fan out (higher is more, finer ones) (0.5 to 2; each look sets its own). |
| sharp | number | 2.2 | How sharp the shafts are against the dark (1 to 4; each look sets its own). |
| dust | number | 1 | How many motes drift in the beams (0 to 2; each look sets its own). |
| blinds | number | 0 | How much the shafts are cut by slats (0 to 1; each look sets its own). |
| interactive | boolean | true | The light source swings a little toward the pointer. |
| 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 swings the light source like the cursor does; scrolling is never blocked.
- Reduced motion
- One still frame of the shafts; the beams and the dust do not drift.
Edge cases and limits
- The bloom at the source is the brightest area: keep text away from it or on a card.
- Blinds read best at 0.3 to 0.7; at 1 the shafts become a grid.
- Dust is only visible inside the beams, so with
sharphigh it becomes sparse by design.
Related

Strands
FreeLines of light in four arrangements: a ribbon, sines that meet at a node, bundles of fine strands, or a veil.
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