Skip to content

FreeIn reviewv1.0.0

Rain Glass

A window in the rain: city lights as bokeh behind misted glass, with beads and drops that slide.
npx shadcn@latest add @vibebox/rain-glass

Controls

Look
1
1
<RainGlass />

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

  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/rain-glass
  3. 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 files
components/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 * (slide > 0.5 ? 0.5 : 0.36)) 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);
  // Beads sit anywhere in their cell, so the glass never shows the grid it was drawn on.
  float y = slide > 0.5 ? 0.0 : (h2(id + ivec2(5, 11)) - 0.5) * (0.62 - 2.0 * rad);
  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

PropTypeDefaultDescription
childrenReactNode—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).
speednumber1Multiplies the preset's speed; 0 holds the scene still.
intensitynumber1Multiplies the preset's brightness, 0.5 to 1.4.
dropsnumber1How many drops sit on the glass (0 to 1.6; each look sets its own).
blurnumber0.75How blurred the street is behind the glass (0.2 to 1; each look sets its own).
fallnumber1How often the drops run (0 to 2; each look sets its own).
interactivebooleantrueThe pointer breathes on the glass and clears the mist around it.
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 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.
  • 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

  • A dusk skyline of lit towers under an orange and violet sky

    A skyline of lit windows against a glowing sky, with a road of light trails in front.

    Backgrounds · WebGL2

↑ ↓ to move↵ to openesc to close