About
Domain warping: q is noise of the position, r is noise of the position bent by q, and the colour is noise of the position bent by r, which gives folds that look like fluid. The gradient is read from that last value through three colours; veins are the inverted absolute of another noise (the creases) raised to a power and masked to the thick of the ink, and a thin bright edge marks where ink meets clear. The pointer rotates the plane around itself with a falloff.
Use it when
- A studio, gallery, label or portfolio wants a living cover.
- A coffee, tea, chocolate or perfume brand wants swirl.
- A luxury or beauty brand wants the marble.
Skip it when
- The page needs a calm, flat field behind small text.
- The brand is playful and bright; use Drift or Bokeh.
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/liquid-ink - 3Use it:
import { LiquidInk } from "@/components/vibebox/liquid-ink"; <LiquidInk preset="ink" className="min-h-[560px] rounded-xl"> <div className="p-12 text-white"> <h1 className="text-4xl font-semibold">Selected work, 2024 to now.</h1> </div> </LiquidInk>
Code
3 filescomponents/vibebox/liquid-ink/index.tsx
/**
* VibeBox Motion · Liquid Ink
* 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 { LiquidInk, type LiquidInkProps, type LiquidInkColor, type LiquidInkValue } from "./liquid-ink.client";
export { PRESET_NAMES as LIQUID_INK_PRESETS, presets as liquidInkPresets, type LiquidInkPreset } from "./liquid-ink.shader";components/vibebox/liquid-ink/liquid-ink.client.tsx
/**
* VibeBox Motion · Liquid Ink
* 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 { liquidInkPiece, presets, type LiquidInkPreset } from "./liquid-ink.shader";
export type LiquidInkColor = "base" | "inkA" | "inkB" | "vein" | "high";
export type LiquidInkValue = "scaleV" | "flow" | "veins" | "sharp";
export type LiquidInkProps = {
/** What sits on the scene: a heading, a call to action, a whole section. */
children?: ReactNode;
/** A named look: `ink`, `coffee`, `marble`, `indigo`, `rose`. */
preset?: LiquidInkPreset;
/** Overrides single palette colours with any CSS colour. */
colors?: Partial<Record<LiquidInkColor, 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<LiquidInkValue, number>>;
const POSTER: PosterSpec = { type: "linear", from: "base", to: "inkA", angle: 135, blobs: [{ color: "inkB", at: [70, 30], size: 55 }, { color: "vein", at: [35, 70], size: 22 }] };
export function LiquidInk({ children, preset, colors, speed, intensity, interactive = true, label, still, className, scaleV, flow, veins, sharp }: LiquidInkProps) {
return (
<BackdropCanvas
slug="liquid-ink"
piece={liquidInkPiece}
presets={presets}
fallback="ink"
preset={preset}
colors={colors}
values={{ scaleV, flow, veins, sharp }}
speed={speed}
intensity={intensity}
poster={POSTER}
scale={0.5}
stillTime={3}
still={still}
interactive={interactive}
label={label}
className={className}
>
{children}
</BackdropCanvas>
);
}components/vibebox/liquid-ink/liquid-ink.shader.ts
/**
* VibeBox Motion · Liquid Ink
* 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";
/*
* Ink dropped in water, or marble: the plane is bent by its own noise twice (so it folds like
* smoke), a gradient of three colours runs through the folds, bright veins ride the creases where
* the fluid is thinnest, and the pointer stirs a small whirl.
*/
const SCENE = /* glsl */ `
uniform vec3 uBase;
uniform vec3 uInkA;
uniform vec3 uInkB;
uniform vec3 uVein;
uniform vec3 uHigh;
uniform float uScaleV;
uniform float uFlow;
uniform float uVeins;
uniform float uSharp;
uniform float uSpeed;
uniform float uIntensity;
float T;
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 whirl around the pointer.
vec2 rel = uv - pu;
float rr = length(rel);
float swirl = uHover * 1.6 * exp(-rr * rr * 7.0);
uv = pu + rot(swirl) * rel;
vec2 p = uv * uScaleV;
vec2 q = vec2(fbm(p + vec2(0.0, T * 0.05)), fbm(p + vec2(5.2, 1.3) - T * 0.04));
vec2 r = vec2(fbm(p + uFlow * 2.4 * q + vec2(1.7, 9.2) + T * 0.06), fbm(p + uFlow * 2.4 * q + vec2(8.3, 2.8) - T * 0.05));
float f = fbm(p + uFlow * 2.4 * r);
// The gradient through the folds.
vec3 col = lin(uBase);
col = mix(col, lin(uInkA), smoothstep(0.32, 0.62, f));
col = mix(col, lin(uInkB), smoothstep(0.58, 0.86, f + 0.2 * (q.x - 0.5)));
// Veins where the folds crease: fine, bright and only in the thick of the ink.
float crease = 1.0 - abs(2.0 * fbm(p * 1.1 + 2.0 * r) - 1.0);
float vein = pow(clamp(crease, 0.0, 1.0), uSharp + 2.0);
col += lin(uVein) * vein * uVeins * 0.55 * smoothstep(0.35, 0.7, f);
// A thin bright edge where the ink meets clear water.
float edge = smoothstep(0.035, 0.0, abs(f - 0.5));
col += lin(uHigh) * edge * 0.22;
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 = ["ink", "coffee", "marble", "indigo", "rose"] as const;
export type LiquidInkPreset = (typeof PRESET_NAMES)[number];
export const presets: Record<LiquidInkPreset, ScenePreset> = {
ink: {
colors: { base: "#05030f", inkA: "#2a1a7a", inkB: "#7a4cff", vein: "#9fe7ff", high: "#ffffff" },
values: { scaleV: 0.95, flow: 1, veins: 0.9, sharp: 5, speed: 0.7, intensity: 1 },
},
coffee: {
colors: { base: "#1a0e08", inkA: "#4a2a17", inkB: "#a8693a", vein: "#f4dcc0", high: "#fff4e4" },
values: { scaleV: 0.9, flow: 1, veins: 0.7, sharp: 4, speed: 0.55, intensity: 1.05 },
},
marble: {
colors: { base: "#f6f3ee", inkA: "#d3cdc3", inkB: "#8f887e", vein: "#b8892f", high: "#ffffff" },
values: { scaleV: 0.85, flow: 0.9, veins: 1.5, sharp: 6, speed: 0.4, intensity: 1 },
},
indigo: {
colors: { base: "#02060f", inkA: "#0c2a5a", inkB: "#1f6fd1", vein: "#6fe3ff", high: "#d8f6ff" },
values: { scaleV: 1.0, flow: 1.1, veins: 0.8, sharp: 5, speed: 0.75, intensity: 1 },
},
rose: {
colors: { base: "#14060d", inkA: "#6a1a3a", inkB: "#ff5f9f", vein: "#ffd0e0", high: "#fff0f5" },
values: { scaleV: 0.95, flow: 1, veins: 0.8, sharp: 5, speed: 0.65, intensity: 1 },
},
};
export const liquidInkPiece = createScenePiece("liquid-ink", fragment);Props
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | What sits on the scene: a heading, a call to action, a whole section. |
| preset | "ink" | "coffee" | "marble" | "indigo" | "rose" | "ink" | The look: ink, coffee, marble, indigo, rose. |
| colors | { base?: string; inkA?: string; inkB?: string; vein?: string; high?: string } | — | Overrides single palette colours with any CSS colour: base (the clear fluid), inkA (the first ink colour), inkB (the second ink colour), vein (the veins), high (the thin bright edge). |
| 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. |
| scaleV | number | 0.95 | How large the folds are (larger is finer) (0.5 to 2; each look sets its own). |
| flow | number | 1 | How strongly the fluid folds on itself (0.4 to 1.6; each look sets its own). |
| veins | number | 0.9 | How bright the veins are (0 to 2; each look sets its own). |
| sharp | number | 5 | How fine the veins are (2 to 9; each look sets its own). |
| interactive | boolean | true | The pointer stirs a whirl in the fluid. |
| 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 fluid; it does not flow.
Edge cases and limits
- The veins are the brightest detail: keep small text off the creases.
- Marble is pale, so it needs dark text rather than white.
- More than
flow1.4 turns to noise.
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