Skip to content

FreeIn reviewv1.0.0

Satin

Folded silk lit by a light you move with the pointer: soft folds, a sheen along them.
npx shadcn@latest add @vibebox/satin

Controls

1
1
<Satin />

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

  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/satin
  3. 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 files
components/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;

float cloth(vec2 p) {
  float h = 0.0;
  h += 0.55 * sin(p.x * 1.6 + 0.9 * sin(p.y * 0.8 + T * 0.18) + T * 0.1);
  h += 0.35 * sin(dot(p, vec2(0.8, 1.3)) * 1.7 + T * 0.14 + 1.3);
  h += 0.22 * sin(dot(p, vec2(-1.2, 0.5)) * 2.6 - T * 0.12 + 4.0);
  h += 0.35 * (fbm3(p * 0.9 + vec2(T * 0.03, 0.0)) - 0.5);
  return h;
}

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);
  float hx = cloth(p + vec2(e, 0.0));
  float hy = cloth(p + vec2(0.0, e));
  vec3 n = normalize(vec3((h - hx) / e * 0.42 * uDepthS, (h - hy) / e * 0.42 * uDepthS, 1.0));

  // The light follows the pointer; at rest it sits upper left.
  vec2 lp = mix(vec2(-0.35, 0.35), vec2((uPointer.x - 0.5) * aspect, uPointer.y - 0.5), uHover * 0.85);
  vec3 L = normalize(vec3(lp - uv, 0.55));
  vec3 V = vec3(0.0, 0.0, 1.0);
  vec3 H = normalize(L + V);
  float diff = clamp(dot(n, L), 0.0, 1.0);
  float spec = pow(clamp(dot(n, H), 0.0, 1.0), 38.0 * uGloss);
  // Anisotropy: the highlight stretches along the fold's direction.
  float along = pow(1.0 - abs(n.x * 0.8 + n.y * 0.6), 6.0);

  vec3 col = mix(lin(uShadow), lin(uCloth), clamp(0.12 + 0.95 * diff * diff + 0.1, 0.0, 1.0));
  col += lin(uSheen) * (spec * 0.85 + along * 0.14 * diff);
  col += lin(uLight) * spec * 0.2;
  // Cloth darkens in the deepest folds.
  col *= 0.78 + 0.34 * smoothstep(-0.7, 0.7, h);

  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 = ["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

PropTypeDefaultDescription
childrenReactNode—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).
speednumber1Multiplies the preset's speed; 0 holds the scene still.
intensitynumber1Multiplies the preset's brightness, 0.5 to 1.4.
foldsnumber1How many folds cross the frame (0.5 to 2; each look sets its own).
depthSnumber1How deep the folds are (0.3 to 1.6; each look sets its own).
glossnumber1How tight the highlight is (0.4 to 2; each look sets its own).
interactivebooleantrueThe pointer is the light moving across the cloth.
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 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 gloss makes thin, harsh highlights; stay under 1.5.
  • 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