About
Two things stacked. Behind: two depths of soft light discs over a faint street glow, blurred by a blur value that is also the mist. In front: three layers of drops on grids. The largest layer slides, with an eased run, a stop, and a trail of clear glass above each drop; the two finer layers are beads that sit still. Each drop is a weak lens: it offsets and flips the street through it, samples it less blurred than the mist, and carries a thin dark edge and a bright catch of light.
Use it when
- A bar, restaurant, cafe, hotel or nightlife page wants atmosphere in a hero.
- A ride, delivery or late-night service wants the rainy city.
- A film, music or event page wants mood without video.
Skip it when
- The brand is bright and daytime.
- The page is dense with data: the drops compete with small text.
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/rain-glass - 3Use it:
import { RainGlass } from "@/components/vibebox/rain-glass"; <RainGlass preset="neon" className="min-h-[560px] rounded-xl"> <div className="p-12 text-white"> <h1 className="text-4xl font-semibold">Open late, rain or not.</h1> </div> </RainGlass>
Code
3 filescomponents/vibebox/rain-glass/index.tsx
/**
* VibeBox Motion · Rain Glass
* 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 { RainGlass, type RainGlassProps, type RainGlassColor, type RainGlassValue } from "./rain-glass.client";
export { PRESET_NAMES as RAIN_GLASS_PRESETS, presets as rainGlassPresets, type RainGlassPreset } from "./rain-glass.shader";components/vibebox/rain-glass/rain-glass.client.tsx
/**
* VibeBox Motion · Rain Glass
* 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 { rainGlassPiece, presets, type RainGlassPreset } from "./rain-glass.shader";
export type RainGlassColor = "glass" | "lightA" | "lightB" | "lightC" | "mist";
export type RainGlassValue = "drops" | "blur" | "fall";
export type RainGlassProps = {
/** What sits on the scene: a heading, a call to action, a whole section. */
children?: ReactNode;
/** A named look: `neon`, `amber`, `midnight`, `overcast`. */
preset?: RainGlassPreset;
/** Overrides single palette colours with any CSS colour. */
colors?: Partial<Record<RainGlassColor, 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<RainGlassValue, number>>;
const POSTER: PosterSpec = { type: "linear", from: "glass", to: "mist", angle: 180, blobs: [{ color: "lightA", at: [20, 70], size: 28 }, { color: "lightB", at: [58, 62], size: 32 }, { color: "lightC", at: [82, 78], size: 26 }] };
export function RainGlass({ children, preset, colors, speed, intensity, interactive = true, label, still, className, drops, blur, fall }: RainGlassProps) {
return (
<BackdropCanvas
slug="rain-glass"
piece={rainGlassPiece}
presets={presets}
fallback="neon"
preset={preset}
colors={colors}
values={{ drops, blur, fall }}
speed={speed}
intensity={intensity}
poster={POSTER}
scale={0.5}
stillTime={3}
still={still}
interactive={interactive}
label={label}
className={className}
>
{children}
</BackdropCanvas>
);
}components/vibebox/rain-glass/rain-glass.shader.ts
/**
* VibeBox Motion · Rain Glass
* 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 window in the rain: city lights as soft discs behind misted glass, and drops that bead on it
* and slide, each leaving a clear trail. Every drop is a little lens: it shows the lights behind
* it upside down and sharper than the mist does. The pointer breathes on the glass, so the drops
* near it gather and run.
*/
const SCENE = /* glsl */ `
uniform vec3 uGlass;
uniform vec3 uLightA;
uniform vec3 uLightB;
uniform vec3 uLightC;
uniform vec3 uMist;
uniform float uDrops;
uniform float uBlur;
uniform float uFall;
uniform float uSpeed;
uniform float uIntensity;
float T;
/* The street behind the glass: three depths of soft lights over a warm glow at the bottom. */
vec3 street(vec2 uv, float soft) {
// A faint glow of the street at the bottom, deeper glass above.
vec3 col = mix(lin(uLightA) * 0.16 + lin(uMist) * 0.25, lin(uGlass) + lin(uMist) * 0.12, smoothstep(-0.5, 0.25, uv.y));
for (int l = 0; l < 2; l++) {
float fl = float(l);
float scale = 5.2 + fl * 5.0;
vec2 p = uv * scale + vec2(fl * 9.0 + T * 0.01, fl * 4.0);
ivec2 id = ivec2(floor(p));
for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++) {
ivec2 c = id + ivec2(x, y);
if (h2(c + ivec2(5, int(fl) * 13)) > 0.5) continue;
vec2 o = h22(c);
vec2 center = vec2(float(x), float(y)) + 0.2 + o * 0.6;
float rad = (0.1 + 0.15 * h2(c + ivec2(9, 4))) * (0.9 + 0.8 * soft);
float d = length(fract(p) - center) / rad;
float pick = h2(c + ivec2(2, 8));
vec3 colr = pick < 0.34 ? lin(uLightA) : pick < 0.67 ? lin(uLightB) : lin(uLightC);
float edge = mix(0.12, 0.98, soft);
float disc = smoothstep(1.0, 1.0 - edge, d);
col += colr * disc * disc * (0.5 + 0.5 * pick) * (1.0 - fl * 0.2) * (0.55 + 0.45 * (1.0 - soft));
}
}
}
return col;
}
/* One layer of drops on a grid: beads that sit still, and drops that slide down with a trail.
Returns the lens offset (xy), the drop mask (z) and a rim highlight (w). */
vec4 drops(vec2 uv, float size, float seed, float slide) {
vec2 p = uv / size;
ivec2 id = ivec2(floor(p)) + ivec2(int(seed) * 41, int(seed) * 7);
vec2 f = fract(p) - 0.5;
float live = h2(id);
if (live > uDrops * 0.55) return vec4(0.0);
float r1 = h2(id + ivec2(3, 1));
float r2 = h2(id + ivec2(8, 6));
float rad = (0.14 + 0.16 * r1);
float x = (r2 - 0.5) * (0.62 - 2.0 * rad) ;
float phase = fract(T * (0.015 + 0.04 * r1) * uFall + r2 * 7.0);
float y = 0.0;
float alpha = 1.0;
float trail = 0.0;
if (slide > 0.5) {
// Gravity: slow, then a run, then it stops.
float s = phase * phase * (3.0 - 2.0 * phase);
float travel = 0.5 - rad;
y = mix(travel, -travel, s);
alpha = smoothstep(0.0, 0.08, phase) * smoothstep(1.0, 0.9, phase);
// The trail above the drop: narrow, fading upward, clear glass.
float above = f.y - y;
trail = smoothstep(0.045, 0.0, abs(f.x - x)) * smoothstep(0.0, 0.05, above) * smoothstep(0.52, 0.0, above) * step(f.y, 0.5) * 0.7;
}
vec2 d = (f - vec2(x, y)) / rad;
// Drops stretch a little along their fall.
d.y /= 1.0 + 0.25 * slide * (1.0 - phase);
float len = length(d);
float mask = smoothstep(1.0, 0.82, len) * alpha;
// A weak lens: it shifts and flips the street a little rather than swallowing it.
vec2 off = -d * 0.32 * mask;
float rim = pow(clamp(len, 0.0, 1.0), 3.0) * mask;
off += vec2(0.0, 0.1) * trail * 0.3;
return vec4(off * size * 1.5, max(mask, trail * alpha), rim);
}
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);
// Mist on the glass is blurrier than the lights; the pointer's breath makes it a little clearer.
float breath = uHover * exp(-length(uv - pu) * 3.0);
float soft = clamp(uBlur - 0.35 * breath, 0.0, 1.0);
vec4 a = drops(uv, 0.11, 1.0, 1.0);
vec4 b = drops(uv + vec2(0.37, 0.19), 0.058, 2.0, 0.0);
vec4 c = drops(uv + vec2(0.11, 0.41), 0.034, 3.0, 0.0);
vec2 off = a.xy + b.xy * 0.7 + c.xy * 0.5;
float mask = max(a.z, max(b.z * 0.9, c.z * 0.7));
// A drop shows the street through a lens, sharper than the misted glass.
float sharp = mix(soft, soft * 0.25, mask);
vec3 col = street(uv + off, sharp);
// The misted glass lifts the dark toward the mist colour.
col = mix(col, lin(uMist) * 0.35 + col * 0.8, (1.0 - mask) * 0.55 * soft);
// Drops: a thin dark edge and a bright catch of light on it.
float rim = max(a.w, max(b.w * 0.8, c.w * 0.6));
col *= 1.0 - 0.12 * rim;
col += lin(uLightC) * pow(rim, 2.0) * 0.4 * smoothstep(0.1, 0.9, 0.5 + 0.5 * normalize(off + vec2(0.0001)).y);
col += vec3(1.0) * mask * 0.035;
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 = ["neon", "amber", "midnight", "overcast"] as const;
export type RainGlassPreset = (typeof PRESET_NAMES)[number];
export const presets: Record<RainGlassPreset, ScenePreset> = {
neon: {
colors: { glass: "#0a0716", lightA: "#ff3fa4", lightB: "#35d8ff", lightC: "#ffd23f", mist: "#2a1a4a" },
values: { drops: 1, blur: 0.75, fall: 1, speed: 0.8, intensity: 1.15 },
},
amber: {
colors: { glass: "#140a05", lightA: "#ffa63d", lightB: "#ff6a2a", lightC: "#ffe0a0", mist: "#3a2412" },
values: { drops: 1, blur: 0.7, fall: 0.9, speed: 0.7, intensity: 1.15 },
},
midnight: {
colors: { glass: "#03060f", lightA: "#5aa8ff", lightB: "#9fd0ff", lightC: "#e8f3ff", mist: "#10203a" },
values: { drops: 1.1, blur: 0.8, fall: 1.1, speed: 0.8, intensity: 1.15 },
},
overcast: {
colors: { glass: "#1a1f26", lightA: "#c9d4e0", lightB: "#8fa3b8", lightC: "#ffffff", mist: "#4a5666" },
values: { drops: 1, blur: 0.85, fall: 1, speed: 0.7, intensity: 1.1 },
},
};
export const rainGlassPiece = createScenePiece("rain-glass", fragment);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | What sits on the scene: a heading, a call to action, a whole section. |
| preset | "neon" | "amber" | "midnight" | "overcast" | "neon" | The look: neon, amber, midnight, overcast. |
| colors | { glass?: string; lightA?: string; lightB?: string; lightC?: string; mist?: string } | — | Overrides single palette colours with any CSS colour: glass (the dark behind the lights), lightA (the main light colour), lightB (the second light colour), lightC (the third light colour and the drops' highlight), mist (the misted 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. |
| drops | number | 1 | How many drops sit on the glass (0 to 1.6; each look sets its own). |
| blur | number | 0.75 | How blurred the street is behind the glass (0.2 to 1; each look sets its own). |
| fall | number | 1 | How often the drops run (0 to 2; each look sets its own). |
| interactive | boolean | true | The pointer breathes on the glass and clears the mist around it. |
| 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; no drops run and the lights do not breathe.
Edge cases and limits
- Keep copy large and short; the drops pass behind it.
- The neon look is the busiest; use midnight or overcast under longer text.
- The drops are cheap but the many beads need the stage at half resolution: do not raise
scale.
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