About
A height field from three travelling sines, one of them bent by another, and a little noise. Its slope gives a normal; a diffuse term picks the cloth colour between shadow and light, and a Blinn-style highlight, plus a stretched term that follows the fold direction, adds the sheen. The light sits upper left at rest and moves to the pointer when it is near.
Use it when
- A fashion, jewellery, bridal, beauty or fragrance page wants luxury without a photo.
- A law, wealth or private-banking page wants quiet richness.
- A gifting page wants the rose or pearl.
Skip it when
- The brand is rugged, technical or playful.
- The page needs strong, saturated colour blocks.
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/satin - 3Use it:
import { Satin } from "@/components/vibebox/satin"; <Satin preset="champagne" className="min-h-[560px] rounded-xl"> <div className="p-12 text-white"> <h1 className="text-4xl font-semibold">Made slowly, worn for years.</h1> </div> </Satin>
Code
3 filescomponents/vibebox/satin/index.tsx
/**
* VibeBox Motion · Satin
* 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 { Satin, type SatinProps, type SatinColor, type SatinValue } from "./satin.client";
export { PRESET_NAMES as SATIN_PRESETS, presets as satinPresets, type SatinPreset } from "./satin.shader";components/vibebox/satin/satin.client.tsx
/**
* VibeBox Motion · Satin
* 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 { satinPiece, presets, type SatinPreset } from "./satin.shader";
export type SatinColor = "cloth" | "shadow" | "sheen" | "light";
export type SatinValue = "folds" | "depthS" | "gloss";
export type SatinProps = {
/** What sits on the scene: a heading, a call to action, a whole section. */
children?: ReactNode;
/** A named look: `champagne`, `rose`, `navy`, `pearl`, `emerald`. */
preset?: SatinPreset;
/** Overrides single palette colours with any CSS colour. */
colors?: Partial<Record<SatinColor, 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<SatinValue, number>>;
const POSTER: PosterSpec = { type: "linear", from: "shadow", to: "cloth", angle: 135, blobs: [{ color: "sheen", at: [28, 22], size: 45 }] };
export function Satin({ children, preset, colors, speed, intensity, interactive = true, label, still, className, folds, depthS, gloss }: SatinProps) {
return (
<BackdropCanvas
slug="satin"
piece={satinPiece}
presets={presets}
fallback="champagne"
preset={preset}
colors={colors}
values={{ folds, depthS, gloss }}
speed={speed}
intensity={intensity}
poster={POSTER}
scale={0.5}
stillTime={3}
still={still}
interactive={interactive}
label={label}
className={className}
>
{children}
</BackdropCanvas>
);
}components/vibebox/satin/satin.shader.ts
/**
* VibeBox Motion · Satin
* 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";
/*
* Folded silk: a few broad folds and a finer crumple make a height field, whose slope lights the
* cloth. A soft diffuse term gives the body of the colour, and a stretched highlight rides the
* folds (the way satin catches light along the weave) in the sheen colour. The pointer is a light
* that you move across the cloth.
*/
const SCENE = /* glsl */ `
uniform vec3 uCloth;
uniform vec3 uShadow;
uniform vec3 uSheen;
uniform vec3 uLight;
uniform float uFolds;
uniform float uDepthS;
uniform float uGloss;
uniform float uSpeed;
uniform float uIntensity;
float T;
/* Long folds that run across the frame with a slow swell, in three sizes that never leave the
same direction: drawn silk, not a wave pool. */
float cloth(vec2 p) {
p.y += 0.32 * sin(p.x * 0.85 + T * 0.1) + 0.18 * sin(p.x * 1.75 - T * 0.07 + 1.0);
float u = p.x * 0.38 + p.y * 0.92;
float h = sin(u * 3.4 + 1.1 * sin(p.x * 0.8 + T * 0.08) + T * 0.1);
h += 0.5 * sin(u * 6.8 + p.x * 0.9 - T * 0.08 + 1.7);
h += 0.22 * sin(u * 12.5 - p.x * 1.5 + T * 0.06);
h += 0.1 * (fbm3(p * 1.4 + T * 0.02) - 0.5);
return h * 0.5;
}
void main() {
vec2 uv = centred(gl_FragCoord.xy);
T = uTime * uSpeed;
float aspect = uResolution.x / uResolution.y;
vec2 p = uv * uFolds * 2.0;
float e = 0.012;
float h = cloth(p);
vec2 grad = vec2(cloth(p + vec2(e, 0.0)) - h, cloth(p + vec2(0.0, e)) - h) / e;
vec3 n = normalize(vec3(-grad * 0.62 * uDepthS, 1.0));
// The light follows the pointer; at rest it sits upper left.
vec2 lp = mix(vec2(-0.55, 0.45), vec2((uPointer.x - 0.5) * aspect, uPointer.y - 0.5), uHover * 0.85);
vec3 L = normalize(vec3(lp - uv, 0.7));
vec3 H = normalize(L + vec3(0.0, 0.0, 1.0));
// Satin shows each fold as a band: dark where it turns from the light, body colour across the
// middle, then a bright band and a fall back. The band is read from the fold's lean toward L.
float lean = dot(n.xy, normalize(L.xy + 1e-4));
float lit = 0.5 + 0.5 * clamp(lean * 1.7, -1.0, 1.0);
vec3 col = mix(lin(uShadow), lin(uCloth), smoothstep(0.08, 0.62, lit));
col += lin(uSheen) * smoothstep(0.62, 0.86, lit) * 0.5;
float spec = pow(clamp(dot(n, H), 0.0, 1.0), 52.0 * uGloss);
col += lin(uSheen) * spec * 0.8 + lin(uLight) * spec * spec * 0.4;
// Cloth is a touch darker where it sits low between folds.
col *= 0.8 + 0.3 * smoothstep(-0.9, 0.9, h);
col *= uIntensity;
col *= 1.0 - 0.3 * smoothstep(0.5, 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 = ["champagne", "rose", "navy", "pearl", "emerald"] as const;
export type SatinPreset = (typeof PRESET_NAMES)[number];
export const presets: Record<SatinPreset, ScenePreset> = {
champagne: {
colors: { cloth: "#d9b98a", shadow: "#3a2a18", sheen: "#fff1d0", light: "#ffffff" },
values: { folds: 1, depthS: 1, gloss: 1, speed: 0.7, intensity: 1.05 },
},
rose: {
colors: { cloth: "#d98aa3", shadow: "#3d1220", sheen: "#ffe0ea", light: "#ffffff" },
values: { folds: 1.1, depthS: 1, gloss: 1, speed: 0.7, intensity: 1.05 },
},
navy: {
colors: { cloth: "#223a7a", shadow: "#050a1f", sheen: "#9fb8ff", light: "#dbe6ff" },
values: { folds: 1, depthS: 1.1, gloss: 1.1, speed: 0.6, intensity: 1.05 },
},
pearl: {
colors: { cloth: "#e6e2dc", shadow: "#8d8a99", sheen: "#ffffff", light: "#ffffff" },
values: { folds: 0.9, depthS: 0.9, gloss: 0.9, speed: 0.6, intensity: 1 },
},
emerald: {
colors: { cloth: "#0f7a58", shadow: "#021a12", sheen: "#9bf5cf", light: "#e0fff2" },
values: { folds: 1, depthS: 1.1, gloss: 1.1, speed: 0.65, intensity: 1.05 },
},
};
export const satinPiece = createScenePiece("satin", fragment);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | What sits on the scene: a heading, a call to action, a whole section. |
| preset | "champagne" | "rose" | "navy" | "pearl" | "emerald" | "champagne" | The look: champagne, rose, navy, pearl, emerald. |
| colors | { cloth?: string; shadow?: string; sheen?: string; light?: string } | — | Overrides single palette colours with any CSS colour: cloth (the cloth in the light), shadow (the cloth in the folds), sheen (the highlight along the folds), light (the lamp's own tint). |
| 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. |
| folds | number | 1 | How many folds cross the frame (0.5 to 2; each look sets its own). |
| depthS | number | 1 | How deep the folds are (0.3 to 1.6; each look sets its own). |
| gloss | number | 1 | How tight the highlight is (0.4 to 2; each look sets its own). |
| interactive | boolean | true | The pointer is the light moving across the cloth. |
| 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 cloth; the folds do not move.
Edge cases and limits
- Pearl is nearly white: use dark text.
- The sheen is the bright part and moves with the light: keep small text off it.
- High
glossmakes thin, harsh highlights; stay under 1.5.
Related

Flow Lines
FreeDozens of thin lines of light riding one slow wave, bunching where they cross, with a glow that follows the pointer.
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