Loading page…
Loading page…
ShaderBackground is a live WebGL canvas behind the content — twenty-one beUI shader variants behind one discriminated-union prop, each with its own uniforms. It buys real atmospheric depth that no gradient reproduces, and it costs a shader runtime and a running render loop. HarborWaters is the one tuning the house ships: plate-black water under sparse gold caustics, drawn from tokens, holding the lock screen and the sign-in takeover.
Two tones over the same water variant. HarborWaters resolves its colours from --ch-* at runtime and re-resolves when the theme attribute flips, so no hex lives in the component. It paints a token gradient floor first, then lazy-loads the shader over it — SSR, first paint and no-WebGL machines see water-coloured depth, never an empty box.
Plate black under sparse gold caustics — the default takeover mood.
The same water with the gold pushed up, for Harbor Mode moments.
import { HarborWaters } from "@coldharbor/webui";
// Absolutely positioned and inert — give it a `relative isolate` parent.
<div className="relative isolate overflow-hidden">
<HarborWaters tone="deep" speed={0.22} size={1.9} className="-z-10" />
{/* content sits above */}
</div>--ch-frame-bg--ch-primary--ch-primary-bright--ch-bgspeed drives the caustic drift; size scales the caustics — smaller reads as deeper water. speed={0} stops the render loop outright, which is what reduced motion resolves to.
Frozen surface. The canvas paints one frame and the rAF loop halts.
Tighter caustics — a deeper, further-down read.
<HarborWaters tone="aurum" speed={0} size={0.9} />--ch-frame-bg--ch-primary--ch-primary-bright--ch-bgTwo surfaces, both full-viewport takeovers with no chrome to compete: the wallet lock screen behind its keypad, and the sign-in surface behind the login form. Both sit at /surfaces/wallet/lock and /surfaces/wallet/login. LockScreen carries it behind a prop — backdrop defaults to the beacon glow, so the canvas is opt-in.
import { LockScreen } from "@coldharbor/webui";
// Default: radial beacon glow over a swell texture. No WebGL.
<LockScreen filled={2} />
// Takeover: the harbour water surface instead.
<LockScreen filled={2} backdrop="waters" />--ch-frame-bg--ch-primary--ch-primary-bright--ch-bgThree variants live, deliberately. Every ShaderBackground is its own canvas and its own render loop; mounting the whole roster at once would put twenty-one of them on one scroll. Colours here come from the compiled token JSON, because WebGL uniforms cannot read CSS variables.
Colour spots drifting under organic distortion — the softest field.
Stripes dragged through a vortex. Reads as current, not decoration.
A static lattice. No speed uniform, no rAF loop, no motion to freeze.
import tokens from "@coldharbor/tokens";
import { ShaderBackground } from "@coldharbor/webui";
const palette = tokens.palette as Record<string, string>;
<ShaderBackground
variant="warp"
colors={[palette.abyss, palette["steel-deep"], palette["aurum-deep"]]}
shape="stripes"
swirl={0.7}
speed={0.25}
/>--ch-bg--ch-border-faintEvery variant the union accepts. The prop type is a discriminated union keyed on variant, so each name unlocks exactly its own uniforms — mesh-gradient takes colors and swirl, water takes caustic and highlights, dot-grid takes gapX. Names marked static carry no speed uniform at all.
mesh-gradientgrain-gradientdot-gridstaticdot-orbitwarpwavesstaticwatervoronoiswirlsmoke-ringstatic-radial-gradientneuro-noisemetaballsgod-raysspiralditheringpulsing-bordercolor-panelsstatic-mesh-gradientsimplex-noiseperlin-noise// SHADER_BACKGROUND_VARIANTS ships the same list at runtime — a client-side
// value, since the component module is "use client".
import { SHADER_BACKGROUND_VARIANTS } from "@coldharbor/webui";
// The union is exhaustive — a wrong prop for a variant is a type error.
<ShaderBackground variant="water" colorHighlight={gold} caustic={0.34} />--ch-fg-faint--ch-border-faintShaderBackground freezes speed to 0 under prefers-reduced-motion — but only for variants that expose speed, since forcing the uniform onto a static shader would be a runtime error rather than a courtesy. dot-grid and waves take no speed; the other nineteen do. At speed 0 the runtime stops requesting frames entirely, so a frozen shader costs nothing per tick. The runtime also pauses when the tab is hidden or the canvas leaves the viewport.
speed is overridden to 0 — one frame, then still.
dot-grid never animated; nothing is touched.
// Inside ShaderBackground — the guard, verbatim in spirit:
const speedProps = reducedMotion && "speed" in props ? { speed: 0 } : {};--ch-bgBe honest about the bill before reaching for it.
What it costs
Where it belongs
// Right: one takeover, lazy runtime, content above the water.
<LockScreen filled={pin.length} backdrop="waters" />
// Wrong: a canvas under a scrolling ledger.
<div className="relative">
<ShaderBackground variant="water" />
<TransactionTable rows={rows} />
</div>--ch-frame-bg--ch-borderHarborWaters on the flat dark layer, colours resolved from live tokens rather than baked in.
--ch-frame-bg--ch-primary--ch-primary-bright--ch-bg