About
Each cell of a grid may hold one particle, whose position inside the cell, size, spin and colour all come from the cell's own hash; neighbours are checked so nothing is cut at a cell edge, and the grid itself scrolls with time, so particles fall or rise through the frame without any state. Seven kinds are seven small shapes: a soft disc, a tumbling ellipse, a flickering spark, a pulsing glow, a turning rectangle, a lit mote and a ring with a highlight. Three scales stand in for depth.
Use it when
- A shop, gifting or seasonal page wants snow, confetti or gold dust.
- A bakery, spa or florist wants petals, embers or bubbles.
- An event or app launch wants a celebration.
Skip it when
- The brand is austere and technical.
- The page has a lot of small text under the near particles.
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/drift - 3Use it:
import { Drift } from "@/components/vibebox/drift"; <Drift preset="snow" className="min-h-[560px] rounded-xl"> <div className="p-12 text-white"> <h1 className="text-4xl font-semibold">A little something for the season.</h1> </div> </Drift>
Code
3 filescomponents/vibebox/drift/drift.client.tsx
/**
* VibeBox Motion · Drift
* 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 { driftPiece, presets, type DriftPreset } from "./drift.shader";
export type DriftColor = "baseD" | "baseD2" | "a" | "b" | "c";
export type DriftValue = "kind" | "amount" | "size" | "fallD" | "glowD";
export type DriftProps = {
/** What sits on the scene: a heading, a call to action, a whole section. */
children?: ReactNode;
/** A named look: `snow`, `petals`, `embers`, `fireflies`, `confetti`, `gold`, `bubbles`. */
preset?: DriftPreset;
/** Overrides single palette colours with any CSS colour. */
colors?: Partial<Record<DriftColor, 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<DriftValue, number>>;
const POSTER: PosterSpec = { type: "linear", from: "baseD", to: "baseD2", angle: 180, blobs: [{ color: "a", at: [30, 40], size: 12 }, { color: "b", at: [70, 60], size: 14 }] };
export function Drift({ children, preset, colors, speed, intensity, interactive = true, label, still, className, kind, amount, size, fallD, glowD }: DriftProps) {
return (
<BackdropCanvas
slug="drift"
piece={driftPiece}
presets={presets}
fallback="snow"
preset={preset}
colors={colors}
values={{ kind, amount, size, fallD, glowD }}
speed={speed}
intensity={intensity}
poster={POSTER}
scale={0.5}
stillTime={3}
still={still}
interactive={interactive}
label={label}
className={className}
>
{children}
</BackdropCanvas>
);
}components/vibebox/drift/drift.shader.ts
/**
* VibeBox Motion · Drift
* 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";
/*
* Things that float: snow, petals, embers, fireflies, confetti, dust or bubbles, in three depths
* so the near ones are large, soft and quick and the far ones small and slow. Every particle is a
* cell's own: its position, size, spin and colour come from the cell, so a field of thousands is
* a handful of lookups per pixel. The pointer is a draught that nudges them.
*/
const SCENE = /* glsl */ `
uniform vec3 uBaseD;
uniform vec3 uBaseD2;
uniform vec3 uA;
uniform vec3 uB;
uniform vec3 uC;
uniform float uKind;
uniform float uAmount;
uniform float uSize;
uniform float uFallD;
uniform float uGlowD;
uniform float uSpeed;
uniform float uIntensity;
float T;
vec3 pal(float r) { return r < 0.4 ? lin(uA) : r < 0.75 ? lin(uB) : lin(uC); }
/* One layer: scale is cells per unit, depth 0 (far) to 1 (near). */
vec3 layer(vec2 uv, float scale, float depth, float seed, vec2 wind) {
int kind = int(uKind + 0.5);
float dir = (kind == 2 || kind == 6) ? 1.0 : -1.0; // embers and bubbles rise, the rest fall
vec2 p = uv * scale;
float fall = uFallD * (0.25 + 0.75 * depth);
p.y -= dir * T * fall * 0.35 * scale * 0.5;
p += wind * depth * scale;
ivec2 id = ivec2(floor(p)) + ivec2(int(seed) * 61, int(seed) * 23);
vec3 sum = vec3(0.0);
for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++) {
ivec2 c = id + ivec2(x, y);
float live = h2(c + ivec2(5, 9));
if (live > uAmount) continue;
float r1 = h2(c + ivec2(1, 2));
float r2 = h2(c + ivec2(7, 3));
float r3 = h2(c + ivec2(4, 8));
// The particle sways about its cell's centre.
vec2 sway = vec2(sin(T * (0.4 + r1) + r2 * 30.0), cos(T * (0.3 + r2) + r3 * 30.0)) * (kind == 3 ? 0.35 : 0.18);
vec2 center = vec2(float(x), float(y)) + 0.2 + 0.6 * vec2(r1, r2) + sway;
vec2 d = fract(p) - center;
float sz = (0.06 + 0.1 * r3) * uSize * (0.6 + 0.8 * depth);
float a = 0.0;
float px = scale * 1.5 / uResolution.y;
vec3 colr = pal(r2);
if (kind == 0) { // snow: a soft disc
a = smoothstep(sz, sz * 0.2, length(d)) * (0.5 + 0.5 * depth);
colr = vec3(1.0) * (0.8 + 0.2 * r1) * lin(uA);
} else if (kind == 1) { // petals: teardrops that tumble and show their colour at the stem
float ang = T * (0.3 + r1) + r2 * 6.28;
vec2 q = rot(ang) * d;
float flip = cos(T * (0.5 + r3) + r1 * 9.0);
q.y /= max(0.22, abs(flip));
float along = clamp(q.x / (sz * 3.2) + 0.5, 0.0, 1.0);
float wid = sz * 0.95 * pow(sin(3.14159 * along), 0.75) * (1.0 - 0.3 * along);
float inside = step(0.0, q.x / (sz * 3.2) + 0.5) * step(q.x / (sz * 3.2) + 0.5, 1.0);
a = smoothstep(px, -px, abs(q.y) - wid) * inside * (0.6 + 0.4 * depth);
colr = mix(lin(uC), lin(uA), smoothstep(0.0, 0.8, along)) * (0.62 + 0.5 * abs(flip));
} else if (kind == 2) { // embers: bright, flickering, cooling as they rise
float tw = 0.5 + 0.5 * sin(T * (2.0 + r1 * 4.0) + r2 * 40.0);
float core = smoothstep(sz * 0.5, 0.0, length(d));
a = (core + 0.3 * exp(-dot(d, d) / (sz * sz * 2.6))) * tw;
colr = mix(lin(uA), lin(uC), r3);
a *= 1.0 + uGlowD;
} else if (kind == 3) { // fireflies: a pulse of light
float pulse = pow(0.5 + 0.5 * sin(T * (0.5 + r1) + r2 * 40.0), 3.0);
a = (smoothstep(sz * 0.55, 0.0, length(d)) + 0.45 * exp(-dot(d, d) / (sz * sz * 6.0))) * pulse;
colr = mix(lin(uA), lin(uB), r3);
} else if (kind == 4) { // confetti: foil that turns over and flashes face-on
float ang = T * (0.5 + r1 * 1.5) + r2 * 6.28;
vec2 q = rot(ang) * d;
float flip = cos(T * (1.2 + r3 * 2.0) + r1 * 9.0);
q.y /= max(0.1, abs(flip));
float edge = max(abs(q.x) - sz, abs(q.y) - sz * 0.5);
float shade = 0.5 + 0.5 * abs(flip);
a = smoothstep(px, -px, edge) * (0.55 + 0.45 * shade);
colr = pal(fract(r1 * 3.0 + r3)) * (0.62 + 0.55 * shade) + vec3(0.35) * pow(abs(flip), 7.0);
} else if (kind == 5) { // dust: tiny motes lit from one side
float lightSide = 0.35 + 0.65 * smoothstep(-0.4, 0.4, d.x * 3.0 + sin(T * 0.2 + r1 * 20.0));
a = smoothstep(sz * 0.4, 0.0, length(d)) * lightSide * 1.2;
colr = mix(lin(uA), vec3(1.0), 0.4);
} else { // bubbles: a thin film that shifts colour, and two catches of light
float l = length(d);
float rim = smoothstep(sz, sz * 0.88, l) * smoothstep(sz * 0.5, sz * 0.92, l);
float film = 0.5 + 0.5 * sin(atan(d.y, d.x) * 2.0 + r1 * 6.28 + T * 0.3);
float body = smoothstep(sz, 0.0, l) * 0.05;
float spec1 = smoothstep(sz * 0.3, 0.0, length(d - vec2(-sz * 0.4, sz * 0.4)));
float spec2 = smoothstep(sz * 0.15, 0.0, length(d - vec2(sz * 0.42, -sz * 0.44))) * 0.5;
a = rim * 0.5 + body + spec1 * 0.8 + spec2;
colr = mix(mix(lin(uA), lin(uC), film), vec3(1.0), 0.4 + 0.45 * spec1);
}
sum += colr * a;
}
}
return sum;
}
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);
// A draught from the pointer: particles near it lean away.
vec2 wind = (uv - pu) * 0.06 * uHover * exp(-length(uv - pu) * 2.2) * 0.0 + vec2(0.0);
vec2 lean = normalize(uv - pu + vec2(0.0001)) * 0.05 * uHover * exp(-length(uv - pu) * 2.5);
vec3 col = mix(lin(uBaseD2), lin(uBaseD), smoothstep(-0.55, 0.5, uv.y));
col += layer(uv + lean, 3.0, 1.0, 1.0, wind) * 1.0;
col += layer(uv + lean * 0.6, 5.4, 0.6, 2.0, wind) * 0.8;
col += layer(uv + lean * 0.3, 9.0, 0.2, 3.0, wind) * 0.55;
col *= uIntensity;
col *= 1.0 - 0.28 * 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 = ["snow", "petals", "embers", "fireflies", "confetti", "gold", "bubbles"] as const;
export type DriftPreset = (typeof PRESET_NAMES)[number];
export const presets: Record<DriftPreset, ScenePreset> = {
snow: {
colors: { baseD: "#2b4a7a", baseD2: "#0d1b36", a: "#ffffff", b: "#dbe9ff", c: "#ffffff" },
values: { kind: 0, amount: 0.7, size: 1, fallD: 0.8, glowD: 0, speed: 0.8, intensity: 1.05 },
},
petals: {
colors: { baseD: "#ffd3de", baseD2: "#f08aa6", a: "#ffffff", b: "#ffe6ed", c: "#ff5f8f" },
values: { kind: 1, amount: 0.6, size: 1.2, fallD: 0.7, glowD: 0, speed: 0.7, intensity: 1 },
},
embers: {
colors: { baseD: "#2a0e08", baseD2: "#050202", a: "#ff7a2a", b: "#ffb347", c: "#ffe9a0" },
values: { kind: 2, amount: 0.6, size: 1, fallD: 1, glowD: 0.4, speed: 0.9, intensity: 1.1 },
},
fireflies: {
colors: { baseD: "#0b2a1c", baseD2: "#020a06", a: "#d9ff6a", b: "#8fffb0", c: "#fff7a0" },
values: { kind: 3, amount: 0.45, size: 1.1, fallD: 0.5, glowD: 0, speed: 0.7, intensity: 1.15 },
},
confetti: {
colors: { baseD: "#2a1a5a", baseD2: "#0d0824", a: "#ff3fa4", b: "#35d8ff", c: "#ffd23f" },
values: { kind: 4, amount: 0.5, size: 1.2, fallD: 1, glowD: 0, speed: 1, intensity: 1.05 },
},
gold: {
colors: { baseD: "#1a1208", baseD2: "#050403", a: "#ffd98a", b: "#ffe9b8", c: "#fff6dc" },
values: { kind: 5, amount: 0.85, size: 1, fallD: 0.35, glowD: 0, speed: 0.6, intensity: 1.25 },
},
bubbles: {
colors: { baseD: "#3aa8d8", baseD2: "#0d4a7a", a: "#dff6ff", b: "#ffffff", c: "#bfeaff" },
values: { kind: 6, amount: 0.4, size: 1.3, fallD: 0.8, glowD: 0, speed: 0.8, intensity: 1.05 },
},
};
export const driftPiece = createScenePiece("drift", fragment);components/vibebox/drift/index.tsx
/**
* VibeBox Motion · Drift
* 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 { Drift, type DriftProps, type DriftColor, type DriftValue } from "./drift.client";
export { PRESET_NAMES as DRIFT_PRESETS, presets as driftPresets, type DriftPreset } from "./drift.shader";Props
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | What sits on the scene: a heading, a call to action, a whole section. |
| preset | "snow" | "petals" | "embers" | "fireflies" | "confetti" | "gold" | "bubbles" | "snow" | The look: snow, petals, embers, fireflies, confetti, gold, bubbles. |
| colors | { baseD?: string; baseD2?: string; a?: string; b?: string; c?: string } | — | Overrides single palette colours with any CSS colour: baseD (the background at the top), baseD2 (the background at the bottom), a (the main particle colour), b (the second particle colour), c (the third particle colour). |
| 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. |
| kind | number | 0 | 0 snow, 1 petals, 2 embers, 3 fireflies, 4 confetti, 5 dust, 6 bubbles (0 to 6; each look sets its own). |
| amount | number | 0.7 | How many particles fill the frame (0.1 to 1; each look sets its own). |
| size | number | 1 | How large the particles are (0.4 to 2; each look sets its own). |
| fallD | number | 0.8 | How fast the particles move (0 to 2; each look sets its own). |
| glowD | number | 0 | How much the embers glow (0 to 1; each look sets its own). |
| interactive | boolean | true | The pointer is a draught that nudges the particles away. |
| 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 particles; they do not move.
Edge cases and limits
- Confetti is busy: use it for a moment, not a page.
- Petals are on a light background: use dark text.
- On phones keep
amountunder 0.6.
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