Add an effect
Effects are complete compositions: an entry shader, a React wrapper with tuned defaults, and the runner that drives the canvas. The registry serves them as shadcn items, so the shadcn CLI copies one into your project with no custom tooling:
npx shadcn@latest add https://vshaders.com/r/mesh-gradient.jsonOr register the namespace once in components.json and add effects by name:
{
"registries": {
"@vshaders": "https://vshaders.com/r/{name}.json"
}
}npx shadcn@latest add @vshaders/mesh-gradientThe registries block lives inside a valid components.json, which the CLI requires before any add. npx shadcn init creates one interactively; in CI or an agent run, write it by hand — this minimal file is enough for a Next.js project (adjust the css path to yours; no Tailwind setup is needed for vshaders items):
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": { "config": "", "css": "app/globals.css", "baseColor": "neutral", "cssVariables": true },
"aliases": { "components": "@/components", "utils": "@/lib/utils", "lib": "@/lib" },
"registries": {
"@vshaders": "https://vshaders.com/r/{name}.json"
}
}What lands in your project
| File | What it is |
|---|---|
shaders/mesh-gradient.wgsl | The entry shader: uniforms, bindings, and the fragment entry point. |
components/mesh-gradient.tsx | A React wrapper with the effect's tuned defaults baked in. |
lib/run-effect.ts | The shared canvas runner: init, surface, effect, frame loop (paused while the canvas is off screen), cleanup. |
lib/shared-gpu.ts | One reference-counted GPU device shared by every effect on the page; the runner leases it and releases on unmount. |
wgsl-env.d.ts | Types .wgsl imports; lands at the project root. |
The CLI also installs the effect's npm dependencies: vgpu, @vgpu/wgsl, @vgpu/wgsl-std, and the @vshaders modules the shader imports. You own the copied files. Edit the shader, rename the component, delete what you don't need; the modules it imports stay versioned dependencies in your package.json. One prerequisite: the shader imports WGSL modules from npm, so the loader from Setup must be wired first.
Not every item has this exact shape: fluid and displacement ship their own runners (lib/fluid-runner.ts, lib/displacement-runner.ts) in place of lib/run-effect.ts, and the filter items (dither, heatmap) ship the filter pass alone. Each item carries install notes — the same text the CLI prints after installing — and they are reproduced on the effect's page under Install, so nothing lives only in your terminal's scrollback.
The editor
Every effect has an editor at /effects/<slug>: the live canvas, the import graph, the full source, and a control for each tunable uniform. Controls are derived from vgpu's reflection of the shader, so they always match the Uniforms struct; the build fails if the two drift. Control labels are human ("Gooeyness"), not uniform names (smoothness) — hover a control to see the name it routes to.
Tuned values encode into the URL hash as #p= followed by base64url JSON; the default state has no hash. A link with a hash reproduces the exact state, so sharing a tuned permalink hands someone the precise look, ready to copy. To take a tuned look into code, use Copy params: it copies the changed values as a params object keyed by real uniform names, in the exact value space the shader consumes — paste it straight into the installed component's params prop, no label-to-uniform translation and no color conversion by hand.
Color spaces
Two conventions coexist, and each effect's source says which it uses. The generative effects (mesh gradient, metaballs, palette field, flow, god rays, fluid, undertones) take color params in linear-light RGB: the shader does its math in linear and converts to sRGB once at the end. The print-flavored filters (dither, heatmap) and ripple's ring are display-referred: their colors are authored in plain sRGB, like inks picked from a swatch book, and map straight to 0..255.
The practical consequence: a hex color from a design tool is sRGB. For a display-referred param, divide by 255 and you are done; for a linear-light param that produces a washed-out look — convert through the sRGB transfer function instead (or skip the math entirely: the editor's color pickers accept hex and Copy params emits ready-to-paste values):
/** #RRGGBB (display sRGB) -> linear-light vec3, the space color params use. */
export function srgbHexToLinear(hex: string): [number, number, number] {
const n = parseInt(hex.replace("#", ""), 16);
const channel = (c: number) =>
c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
return [
channel(((n >> 16) & 255) / 255),
channel(((n >> 8) & 255) / 255),
channel((n & 255) / 255),
];
}Effects that read a texture
Three effects are filters over an input you provide, and each item's install notes spell out its path:
- Dither reads any texture as
srcTexture. Render your scene into an offscreen vgputarget()and bind the target's color — everything stays on the GPU. - Heatmap reads one packed texture (R contour, G outer blur, B inner blur). The shipped
lib/heatmap-prepare.tsbuilds that packing from any logo asImageDataon the CPU. vgpu targets take no direct pixel upload (nocopy_dst), so the item also shipsshaders/heatmap-unpack.wgsl: write the bytes into astorage()buffer, draw one unpack pass into a target, and bind that target assrcTexture. - Displacement shears an image: pass
imageUrlon the installed component (or call the handle'sloadImage) pointing at an asset of your project — the gallery's demo image is not included. Internally it uses the same storage-buffer + unpack-pass bridge, already wired in its runner.
Current effects
- Mesh gradient: four color spots under swirl and distortion, blended as a weighted OKLab sum.
- Metaballs: gooey droplets fusing on black, up to four quiet colors blended across the necks.
- Palette field: layered fBM read as ink on paper, one accent bleeding along the contours.
- Flow: layered fBM with a liquid look; the pointer stirs the pair. One pass of math, not a simulation.
- Ripple: concentric waves radiating from the pointer, each crest shaped by an easing curve.
- Dither: a pixelizing dither filter for any texture you render (ported from Paper Shaders); the demo prints an orbiting sculpture.
- Fluid: a real multi-pass Stable Fluids simulation, driven only by the cursor, contained walls by default.
- Displacement: a fine grid of cells sheared apart by cursor speed, fringing and healing when the cursor rests.
- God rays: volumetric light shafts hanging off the pointer, built from constant-along-ray noise.
- Heatmap: heat flowing through any shape (ported from Paper Shaders); logos via the shipped preparer, models live.
- Undertones: light behind ribbed glass, smeared into streaked bands with specular crests.
All eleven are installable from the registry. Fluid ships its own runner (lib/fluid-runner.ts) in place of lib/run-effect.ts: nine pass shaders land under shaders/fluid/ and the runner drives them as one simulation frame per tick.
Or skip the registry
The module packages work without any of this. Install one and import it from your own entry shader:
pnpm add @vshaders/sdfimport { sdCircle } from "@vshaders/sdf/2d";
import { opSmoothUnion } from "@vshaders/sdf/ops";
struct Uniforms {
resolution: vec2f,
time: f32,
}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@fragment fn main(@builtin(position) position: vec4f) -> @location(0) vec4f {
let res = max(uniforms.resolution, vec2f(1.0));
let p = (position.xy / res - vec2f(0.5)) * vec2f(res.x / res.y, 1.0);
let orbit = vec2f(0.18 * cos(uniforms.time), 0.0);
let d = opSmoothUnion(sdCircle(p - orbit, 0.2), sdCircle(p + orbit, 0.2), 0.1);
return vec4f(vec3f(smoothstep(0.01, -0.01, d)), 1.0);
}vgpu resolves the bare specifiers against your node_modules, and unused exports are pruned from the composed shader. The API references document every function.